线程同步
互斥锁
互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问 完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁
初始化
- 互 斥锁使 用 pthread_mutex_t 数 据类型 表示, pthread_mutex_t 其 实是一个 结构体 类型, 而宏 PTHREAD_MUTEX_INITIALIZER 其实是一个对结构体赋值操作的封装
- 使用宏定义初始化互斥锁:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER |
- 使用函数初始化互斥锁
|
- mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向需要进行初始化操作的互斥锁对象;
- attr:参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用 于定义互斥锁的属性,若将参数 attr 设置为 NULL,则表示将互斥锁的属性设置为 默认值,在这种情况下其实就等价于 PTHREAD_MUTEX_INITIALIZER 这种方式初始化,而不同之处在于, 使用宏不进行错误检查。
- 返回值:成功返回 0;失败将返回一个非 0 的错误码。
pthread_mutex_t mutex; |
互斥锁的属性
- 如果不使用默认属性,在调用 pthread_mutex_init()函数时,参数 attr 必须要指向一个 pthread_mutexattr_t 对象,而不能使用 NULL。当定义 pthread_mutexattr_t 对象之后,需要使用 pthread_mutexattr_init()函数对该 对象进行初始化操作,当对象不再使用时,需要使用 pthread_mutexattr_destroy()将其销毁
|
- 属性的类型
- PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果 线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已 由其它线程锁定,对其解锁会导致不确定结果。
- PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。譬如这三种情况都会导致返 回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次), 返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁 进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试 工具,以发现程序哪里违反了互斥锁使用的基本原则。
- PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行 多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个 互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
- PTHREAD_MUTEX_DEFAULT : 此 类 互 斥 锁 提 供 默 认 的 行 为 和 特 性 。 使 用 宏 PTHREAD_MUTEX_INITIALIZER 初 始 化 的 互 斥 锁 , 或 者 调 用 参 数 arg 为 NULL 的 pthread_mutexattr_init()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大 灵活性, Linux 上 , PTHREAD_MUTEX_DEFAULT 类 型 互 斥 锁 的 行 为 与 PTHREAD_MUTEX_NORMAL 类型相仿。
互斥锁加锁和解锁
- 互斥锁初始化之后,处于一个未锁定状态,调用函数 pthread_mutex_lock()可以对互斥锁加锁、获取互 斥锁,而调用函数 pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。
|
如果互斥锁处于未锁定状态,则此次调用会上锁成 功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock()会一直 阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回
调用 pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:
- 对处于未锁定状态的互斥锁进行解锁操作;
- 解锁由其它线程锁定的互斥锁。
如果线程加锁的时候不希望被阻塞,可以使用
pthread_mutex_trylock()函数- 如果 互斥锁已经被其它线程锁住,调用 pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。
|
销毁互斥锁
- 调用 pthread_mutex_destroy()函数来销毁互斥锁
|
- 不能销毁还没有解锁的互斥锁,否则将会出现错误;
- 没有初始化的互斥锁也不能销毁。
互斥锁死锁
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过 一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;譬如,程序中使用 一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞 状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程 拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。
条件变量
条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条 件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。条件变量包括
- 一个线程等待某个条件满足而被阻塞
- 另一个线程中,条件满足时发出“信号”
条件变量初始化
- 使用宏定义初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; |
- pthread_cond_init()函数原型如下所示
|
使用这些函数需要包含头文件,使用 pthread_cond_init()函数初始化条件变量,当不再 使用时,使用 pthread_cond_destroy()销毁条件变量。
参数 cond 指向 pthread_cond_t 条件变量对象,对于 pthread_cond_init()函数,类似于互斥锁,在初始化 条件变量时设置条件变量的属性,参数 attr 指向一个 pthread_condattr_t 类型对象,pthread_condattr_t 数据类 型用于描述条件变量的属性。可将参数 attr 设置为 NULL,表示使用属性的默认值来初始化条件变量,与使 用 PTHREAD_COND_INITIALIZER 宏相同。
注意事项
- 在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或 者函数 pthread_cond_init()都行;
- 对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
- 对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
- 对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
- 经 pthread_cond_destroy()销毁的条件变量,可以再次调用 pthread_cond_init()对其进行重新初始化。
通知和等待
- 发送信号操作即是通知一个或多个处于等待状态 的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检 查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。
- 函数 pthread_cond_signal()和 pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多 个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。
|
- pthread_cond_signal()函数至少能唤醒一个线程,而 pthread_cond_broadcast()函数 则能唤醒所有线程。
- 使用 pthread_cond_broadcast()函数总能产生正确的结果,唤醒所有等待状态的线程,但函数 pthread_cond_signal()会更为高效,因为它只需确保至少唤醒一个线程即可
|
当程序当中使用条件变量,当判断某个条件不满足时,调用 pthread_cond_wait()函数将线程设置为等待 状态(阻塞)。
cond:指向需要等待的条件变量,目标条件变量;
mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向一个互斥锁对象;前面开头便给大家介绍 了,条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥 锁的保护下进行的,也就是说条件本身是由互斥锁保护的。
- 在 pthread_cond_wait()函数内部会对参数 mutex 所指定的互斥锁进行操作,通常情况下,条件判断以及 pthread_cond_wait()函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁了。调用 pthread_cond_wait()函数时,调用者把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表 上,然后将互斥锁解锁;当 pthread_cond_wait()被唤醒返回时,会再次锁住互斥锁。
如果调用 pthread_cond_signal()和 pthread_cond_broadcast()向指定条件变量发送信号时,若无任何线程等待该条件变量, 这个信号也就会不了了之。
当调用 pthread_cond_broadcast()同时唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁 失败又会陷入阻塞。
|
- 上面的程序实现的功能是主线程对一个变量上互斥锁,+1,解锁互斥锁,然后发送信号唤醒子线程,线程对变量进行加锁,等待信号量,然后对变量-1,然后在解锁。
自旋锁
如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋 锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。 由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁 在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者 是否已经释放了锁,“自旋”一词因此得名。
自旋锁的不足之处在于:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋), 所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。
自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使 得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使 用自旋锁,效率高!
区别
- 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
- 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自 旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自 旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低, 故自旋锁不适用于等待时间比较长的情况。
- 使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为 自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能 被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了 CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!
初始化
- 使用 pthread_spin_init()函数对其 进行初始化,当不再使用自旋锁时,调用 pthread_spin_destroy()函数将其销毁
|
- 参数 lock 指向了需要进行初始化或销毁的自旋锁对象,参数 pshared 表示自旋锁的进程共享属性,可以 取值如下:
- PTHREAD_PROCESS_SHARED:共享自旋锁。该自旋锁可以在多个进程中的线程之间共享;
- PTHREAD_PROCESS_PRIVATE:私有自旋锁。只有本进程内的线程才能够使用该自旋锁。
加锁和解锁
- 可以使用 pthread_spin_lock()函数或 pthread_spin_trylock()函数对自旋锁进行加锁,前者在未获取到锁时 一直“自旋”;对于后者,如果未能获取到锁,就立刻返回错误,错误码为 EBUSY。不管以何种方式加锁, 自旋锁都可以使用 pthread_spin_unlock()函数对自旋锁进行解锁
|
- 如果自旋锁处于未锁定状态,调用 pthread_spin_lock()会将其锁定(上锁),如果其它线程已经将自旋 锁锁住了,那本次调用将会“自旋”等待;如果试图对同一自旋锁加锁两次必然会导致死锁。
|
读写锁
读写锁有如下两个规则:
- 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读 模式加锁还是以写模式加锁)的线程都会被阻塞。
- 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以 写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。当读写锁处于写模式加锁状态 时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁;当读写锁处于 读模式加锁状态时,它所保护的数据就可以被多个获取读模式锁的线程读取。
初始化
- 使用宏定义初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; |
- 其他方式可以使用 pthread_rwlock_init()函数对其进行初始化,当读写锁不再使用时,需要调用 pthread_rwlock_destroy()函数将其销毁
|
- 若将参数 attr 设置为 NULL,则表示将读写锁的属性设置为默认值,在 这种情况下其实就等价于 PTHREAD_RWLOCK_INITIALIZER 这种方式初始化,而不同之处在于,使用宏 不进行错误检查。
上锁和解锁
- 以读模式对读写锁进行上锁,需要调用 pthread_rwlock_rdlock()函数;以写模式对读写锁进行上锁,需 要调用 pthread_rwlock_wrlock()函数。不管是以何种方式锁住读写锁,均可以调用 pthread_rwlock_unlock()函 数解锁
|
当读写锁处于写模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()或 pthread_rwlock_wrlock()函数 均会获取锁失败,从而陷入阻塞等待状态;当读写锁处于读模式加锁状态时,其它线程调用 pthread_rwlock_rdlock()函数可以成功获取到锁,如果调用 pthread_rwlock_wrlock()函数则不能获取到锁,从 而陷入阻塞等待状态。
如果线程不希望被阻塞,可以调用 pthread_rwlock_tryrdlock()和 pthread_rwlock_trywrlock()来尝试加锁, 如果不可以获取锁时。这两个函数都会立马返回错误,错误码为 EBUSY。
|
使用例
|