[linux c/c++] 各种锁之间的差异(自旋锁、互斥量、条件变量、屏障、读写锁)

前言:

锁,作为线程间/进程间系统资源,在应对“多线程” 访问相同 “资源” 场景时,扮演重要角色。用得好,可以有效地为系统解耦各个模块;但是如果用的不好,一来可能造成系统效率低下( 某条并不重要的流水线长期占据资源锁,另外重要的流水线始终无法获得访问资源的权利 ),二来可能导致系统死锁。

自旋锁:

自旋锁一般用在内核中,其逻辑行为是:“使用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

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值