前言:
锁,作为线程间/进程间系统资源,在应对“多线程” 访问相同 “资源” 场景时,扮演重要角色。用得好,可以有效地为系统解耦各个模块;但是如果用的不好,一来可能造成系统效率低下( 某条并不重要的流水线长期占据资源锁,另外重要的流水线始终无法获得访问资源的权利 ),二来可能导致系统死锁。
自旋锁:
自旋锁一般用在内核中,其逻辑行为是:“使用cpu反复执行获取锁的动作,直到获取到锁为止”。这里有几个要点:
1)自旋锁如果未获取到锁,则会立刻再次尝试
2)自旋锁的尝试是同步行为,是会占用cpu的
因此,如果机器是单cpu的,而运行在次cpu上的线程进行了一次自旋锁获取,那么毫无疑问,此线程将独占此cpu,如果自旋处理流程中没有任何能够 让出 时间片的动作,此cpu利用率将飙升至 100%
代码模型:
while (抢锁(lock) == 没抢到) {
//做些什么,比如计数
//当计数到某个值以后,直接跳出循环
}
小结:
1.自旋锁会占用CPU,让其处于忙等状态,同时,还会“阻塞中断”,这很有用,某些相当关键的代码,如果加上自旋锁,那么可以 有效的屏蔽中断的干扰,不过这种场景在用户环境不常见,多出现在内核编程中
2.lock和trylock返回0就表示加锁成功,否则失败
互斥量:
互斥量Mutex的逻辑行为是:“尝试获取互斥量,如果被别的 线程 占用,则当前线程进入休眠状态,直到操作系统帮自己解除休眠”。要点如下:
1)获取互斥量失败时,当前线程会被投入睡眠
2)睡眠的唤醒有操作系统完成,即当前线程表现为阻塞状态(需要 区别 阻塞 和 忙等,线程阻塞不占cpu,线程忙等会再用cpu)。
posix关联接口:
PTHREAD_MUTEX_INITIALIZER : 互斥量的初始值,定义互斥量的时候,可以使用这个只作为初始值。
pthread_mutex_init : 初始化互斥量
对入参的pthread_mutex_t变量进行初始化,猜测具体的动作是注册到内核的监视列表中。
pthread_mutex_destroy :销毁互斥量
对入参的pthread_mutex_t变量进行销毁,猜测底层动作是将互斥量从内核监视列表中去注册。
pthread_mutex_lock : 阻塞锁互斥量
锁住互斥量,如果互斥量没有被其他线程锁住,那么当前线程获得锁,继续往下执行。如果被其他线程锁住,那么当前线程投入睡眠,等待操作系统唤醒自己。唤醒的同时,会把锁交给当前线程。
pthread_mutex_trylock : 尝试锁互斥量
尝试锁互斥量,如果尝试成功,则获得互斥量,同时返回 0 ,否则 非0 以表示错误信息
pthread_mutex_timedlock : 超时阻塞锁互斥量
相对于pthread_mutex_lock,此函数不会一直睡眠下去,当入参中的超时时间到达以后,操作系统会唤醒当前线程,如果在超时时间内获得了互斥量,函数返回0,否则非0,另外返回值有一种情况可以表示是超时返回。
pthread_mutex_unlock : 释放互斥量
释放当前线程持有的互斥量。
条件变量:
条件变量的出现是为了满足特殊的使用场景。其实即便使用互斥量也是可以完成这种场景的实现,只不过使用互斥量会导致代码更复杂且会更吃CPU。
以生产者消费者模型为例: 生产者 ----> 消息队列 ----> 消费者,生产者和消费者正常情况下不会出现在同一个线程中,这样的话,他们对于消息队列的访问就涉及 “竞争”。
如果使用Mutex实现,那么“生产者在插入队列之前需要先加锁,操作完成再解锁”,而消费者 “同样需要先加锁队列,然后从队列中读数据,结束后再解锁队列”。这样的逻辑是OK的,但是在生产者效率明显低于消费者的情况下,这个模型不是好模型,因为生产者的产出能力不强,那么大量的cpu都消耗在消费者的加锁解锁上了,大部分情况下消费者都是无法从队列中读到数据的。
如果使用条件变量实现,就可以很好解决这个问题,区别在于,消费者在第一次加锁pthread_mutex_lock后,会使用pthread_cond_wait释放刚才加的锁,同时自己进入睡眠状态,而投入睡眠的动作同时会把自己唤醒的条件一并告诉内核,当内核发现条件满足,则消费者线程从pthread_cond_wait函数返回。
在“生产者-消费者”模型中,永远考虑优先使用条件变量,而不是Mutex。
注:
条件变量是对Mutex的一个包装,我们在使用的时候是需要创建一个Mutex来配合的,这个Mutex完成加解锁的动作,而条件变量pthread_cond_t更像是一个标志位。
posix关联接口:
pthread_cond_init : 初始化一个条件变量
pthread_cond_destroy : 销毁一个条件变量
pthread_cond_wait : 等待一个条件变量
入参需要指定一个“已锁的”mutex,这个mutex在pthread_cond_wait 内部会被解锁,然后内核会用入参的条件变量对象来锁住当前线程。
pthread_cond_broadcast : 广播唤醒所有pthread_cond_wait 等待的线程
pthread_cond_signal : 随机发送给一个pthread_cond_wait 等待的线程
pthread_cond_timedwait : 超时等待一个条件变量
如果超时,此函数会返回,再次调用时,请保证传入的mutex是加了锁的,这点容易遗漏
注意:
条件变量的 pthread_cond_wait 和 pthread_cond_timedwait 入参中的mutex一定是已经锁上的。
屏障:
屏障的行为模型是:“设置一个计数器,初始化为N,调用pthread_barrier_wait会让这个计数器 - 1,同时将当前线程投入睡眠,一旦计数器的值为0,则内核唤醒所有调用pthread_barrier_wait的线程”。
屏障可以用来实现并发场景,让所有线程准备好数据后进入休眠,当一定数量的线程都通过pthread_barrier_wait进入睡眠后,内核会唤醒所有线程一并启动。
小技巧:
初始化屏障时,一般会给控制线程预留一个名额,在观察所有线程都进入准备状态后,控制线程通过pthread_barrier_wait拆除最后屏障,这样方便业务流程的执行。
注意:
屏障的使用需要考虑线程执行的先后顺序,因此为控制线程预留一个计数是很有必要的。
posix关联接口:
pthread_barrier_init : 初始化屏障,其中有一个参数指定计数最大值
pthread_barrier_wait : 对计数-1,同时将调用线程投入睡眠
ps:
也可以说屏障是增加计数到指定值,也可以说是减少计数到0,意会即可,效果都是一样的,实际的情况要翻源码才知道。
读写锁:
读写锁的行为模型是:
1)当前线程加写锁,则其他线程加写锁动作----->被投入睡眠,同样地加读锁动作----->被投入睡眠;
2)当前线程加读锁,则其他线程加写锁动作----->被投入睡眠,但是加读锁动作--/-->被投入睡眠。
小结:
加写锁,等于Mutex
加读锁,再加写会睡眠,再加读不影响
读锁可以有n个,只要还有一个读锁在,就不能写
类比:
一群人在看书,一个人在写书,只要还有一个读者在看书,写书的人都不能往书里面写东西,否则,其他早早看完的人和最后看完的人看到的内容会有出入。
同一个时刻只能有一个人在写书。
posix关联接口:
pthread_rwlock_init : 初始化
pthread_rwlock_destory : 销毁
pthread_rwlock_rdlock : 加读锁
pthread_rwlock_wrlock : 加写锁
pthread_rwlock_unlock : 借锁
解锁动作不分读写,可以认为同时解除了读写
pthread_rwlock_tryrdlock : 尝试加读锁
pthread_rwlock_trywrlock : 尝试加写锁
pthread_rwlock_timedrdlock :带超时的读锁
pthread_rwlock_timedwrlock :待超时的写锁
注:
读写锁,是指对一个pthread_rwlock_t加不同的锁,不是有两个pthread_rwlock_t