自旋锁与互斥锁

自旋锁与互斥锁
理论分析
互斥锁的问题
自旋锁应用场景
自旋锁实践
总结

自旋锁与互斥锁
自旋锁和互斥锁是多线程程序中的重要概念。 它们被用来锁住一些共享资源, 以防止并发访问这些共享数据时可能导致的数据不一致问题。 但是它们的不同之处在哪里? 我们应该在什么时候用自旋锁代替互斥锁?

理论分析
从理论上说, 如果一个线程尝试加锁一个互斥锁的时候没有成功, 因为互斥锁已经被锁住了, 这个未获取锁的线程会休眠以使得其它线程可以马上运行。 这个线程会一直休眠, 直到持有锁的线程释放了互斥锁, 休眠的线程才会被唤醒。 如果一个线程尝试获得一个自旋锁的时候没有成功, 该线程会一直尝试加锁直到成功获取锁。 因此它不允许其它线程运行(当然, 操作系统会在该线程所在的时间片用完时, 强制切换到其它线程)。

互斥锁的问题
互斥锁存在的问题是, 线程的休眠和唤醒都是相当昂贵的操作, 它们需要大量的CPU指令, 因此需要花费一些时间。 如果互斥量仅仅被锁住很短的一段时间, 用来使线程休眠和唤醒线程的时间会比该线程睡眠的时间还长, 甚至有可能比不断在自旋锁上轮训的时间还长。自旋锁的问题是, 如果自旋锁被持有的时间过长, 其它尝试获取自旋锁的线程会一直轮训自旋锁的状态, 这将非常浪费CPU的执行时间, 这时候该线程睡眠会是一个更好的选择。

自旋锁应用场景
在单核/单CPU系统上使用自旋锁是没用的, 因为当线程尝试获取自旋锁不成功的时候会一直尝试, 这会一直占用CPU, 其它线程不可能运行, 因为其他线程不能运行, 这个锁也就不会被解锁。 换句话说, 在单核/单CPU的系统上,自旋锁除了浪费时间没有一点好处。 这时如果这个线程(记为A)可以休眠, 其它线程可以立即运行, 因为其它有可能解锁, 那么线程A可能在唤醒后继续执行。
在多核/多CPU的系统上, 特别是大量的线程只会短时间的持有锁的时候, 在使线程睡眠和唤醒线程上浪费大量的时间, 也许会显著降低程序的运行性能。 使用自旋锁, 线程可以充分利用调度程序分配的时间片(经常阻塞很短的时间, 不用休眠, 然后马上继续它们的工作了), 以达到更高的处理能力和吞吐量。

自旋锁实践
因为程序员往往并不能事先知道哪种方案会更好(比如, 不知道运行环境的CPU核的数量), 操作系统也不知道一段指令是不是针对单核或者多核环境下做过优化, 所以大部分操作系统并不严格区分互斥锁和自旋锁。 实际上, 绝大部分现代的操作系统采用的是混合型互斥锁(hybrid mutexes)和混合型自旋锁(hybrid spinlocks)。 它们是什么意思呢?
混合型互斥锁, 在多核系统上起初表现的像自旋锁一样, 如果一个线程不能获取互斥锁, 它不会马上被切换为休眠状态, 因为互斥量可能很快就被解锁, 所以这种机制会表现的像自旋锁一样。 只有在一段时间以后(或者尝试一定次数,或者其他指标)还不能获取锁, 它就会被切换为休眠状态。 如果运行在单核/单CPU上, 这种机制将不会自旋(就像上面解释的, 这种情况自旋没有什么好处)。
混合型自旋锁, 起初表现的和正常自旋锁一样, 但是为了避免浪费大量的CPU时间, 会有一个折中的策略。 这种机制不会把线程切换到休眠态(既然想要使用自旋锁, 那么你并不希望这种情况发生), 也许会决定放弃这个线程的执行(马上放弃或者等一段时间)并允许其他线程运行, 这样提高了自旋锁被解锁的可能性(大多数情况, 线程之间的切换操作比使线程休眠而后唤醒它要昂贵, 尽管那不是很明显)。

总结
如果对选择哪种方案感到疑惑, 那就使用互斥锁吧, 并且大多数现代的操作系统都允许在获取锁的时候自旋一段时间(混合型互斥锁)。 只有在一定条件下使用自旋锁才可以提高性能, 事实上, 你现在在做的项目可能没有一个能在通过自旋锁提高性能。 也许你考虑使用你自己定义的”锁对象”, 它可以在内部使用互斥锁或者自旋锁(例如: 在创建锁对象时, 用哪种机制是可配置的), 刚开始在所有的地方都是用互斥锁, 如果你认为在有些地方用自旋锁确实可以提高性能, 你可以试试, 并且比较两种情况的结果(使用一些性能评测工具), 但一定要在单核和多核环境上测试之后再下结论(如果你的代码是夸平台的, 也要尽可能在不同的平台上测试下)。

 

 

其他:读写锁性能

https://blog.csdn.net/xxlblue/article/details/23598805

Linux多线程服务端编程》2.3节这样写道:

读写锁(rwlock)是个看上去很美的抽象,它明确区分了read和write两种行为。

初学者常干的一件事情是,一见到某个共享数据结构频繁读而很少写,就把mutex替换为rwlock。甚至首选rwlock来保护共享状态,这是不正确的。

1、从正确性方面来说,一种典型的易犯错误是在持有read lock的时候修改了共享数据。这通常发生在程序的维护阶段,为了新增功能,程序猿不小心在原来read lock保护的函数中调用了会修改状态的函数。这种错误的后果跟无保护并发读写共享数据是一样的。

2、从性能方面来说,读写锁不见得比普通mutex更高效。无论如何reader lock加锁的开销不会比mutex lock小,因为他要更新当前reader的数目。如果临界区很小,锁竞争不激烈,那么mutex往往会更快。(XXL:如果临界区设置的很大,说明程序本身是有问题的)

3、reader lock可能允许提升(upgrade)为writer lock,也可能不允许提升(Pthread rwlock不允许提升)。如果允许把读锁提升为写锁,后果跟使用recursive mutex(可重入)一样,会造成程序其他问题。如果不允许提升,后果跟使用non-recursive mutex一样,会造成死锁。我宁愿程序死锁,留个“全尸”好查验。

4、通常reader lock是可重入的,writer lock是不可重入的。但是为了防止writer饥饿,writer lock通常会阻塞后来的reader lock,因此reader lock在重入的时候可能死锁。另外,在追求低延迟读取的场合也不适用读写锁。

XXL:补充一下rwlock死锁的问题,线程1获取了读锁,在临界区执行代码;这时,线程2获取写锁,在该锁上等待线程1完成读操作,同事线程2阻塞了后续的读操作;线程1仍在进行剩余读操作,但是它通过函数调用等间接方式,再次获取那个读锁,此时,线程1阻塞,因为线程2已经上了写锁;同时,线程2也在等待线程1释放读锁,才能进行写操作。因此发生了死锁,原因就在于,读锁是可重入的。
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值