如何理解互斥锁,条件锁,读写锁以及自旋锁?

如何理解互斥锁,条件锁,读写锁以及自旋锁?他们之间的区别和应用场景究竟是什么?
关注者
910
被浏览
42108

要正确理解各类线程安全手段,得有操作系统和计算机原理的基本功,才理解原理。

自旋锁:如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直申请CPU时间片轮询自旋锁,直到获取为止,一般应用于加锁时间很短(1ms左右或更低)的场景。

至于实现机制,以linux的自旋锁为例(kernel 2.6.23,asm-i386/spinlock.h),嵌入的汇编源码如下:

line 4: 对lock->slock自减,这个操作是互斥的,LOCK_PREFIX保证了此刻只能有一个CPU访问内存

line 5: 判断lock->slock是否为非负数,如果是跳转到3,即获得自旋锁

line 6: 位置符

line 7: lock->slock此时为负数,说明已经被其他cpu抢占了,cpu休息一会,相当于pause指令

line 8: 继续将lock->slock和0比较,

line 9: 判断lock->slock是否小于等于0,如果判断为真,跳转到2,继续休息

line 10: 此时lock->slock已经大于0,可以继续尝试抢占了,跳转到1

line 11: 位置符

重点是LOCK_PREFIX前缀对CPU独占访问内存的保证,实现方式和cpu架构有关,在x86架构上,早期的x86CPU通过总线锁来实现这一点,现在的x86CPU通过缓存一致性协议来实现(只有一个cache可以对内存进行最终写入)。

另外一个例子,Intel DPDK里的自旋锁:

这里没有LOCK前缀,但用到了xchg指令来交换寄存器和内存间的内容,用了cmpl来对比sl->locked,组合在一起,这段汇编其实就是用来实现OS里的CAS(compare and swap)原语,在x86架构上,同样是通过缓存一致性协议来确保CAS操作的原子性。

互斥锁:无法获取琐时,进线程立刻放弃剩余的时间片并进入阻塞(或者说挂起)状态,同时保存寄存器和程序计数器的内容(保存现场,上下文切换的前半部分),当可以获取锁时,进线程激活,等待被调度进CPU并恢复现场(上下文切换下半部分)
上下文切换会带来数十微秒的开销,不要在性能敏感的地方用互斥锁

读写锁:写锁被占用时,所有申请读锁和写锁的进线程都会被阻塞,读锁被占用时,申请写锁的进线程被阻塞,其他申请读锁的进线程不会。

读写自旋锁:同时集成自旋锁和读写锁的优点,tbb库提供该锁。