自旋锁和相互排斥锁是多线程编程中的两个重要概念。他们都能用来锁定一些共享资源,以阻止影响数据一致性的并发訪问。可是他们之间确实存在差别,那么这些差别是什么?
1 理论
理论上,当一个线程试图获取一个被锁定的相互排斥锁时,该操作会失败然后该线程会进入睡眠,这样就能立即让还有一个线程执行。当持有相互排斥锁的线程释放该锁之后,进入睡眠状态的线程就会被唤醒。可是,当一个线程试图获取一个自旋锁而没有成功时,该线程会不断地重试,直到终于成功为止;因此该线程不会将执行权交到其它线程手中(当然,一旦当前线程的时间片超时,操作系统会强行切换到还有一个线程)。
2 问题
相互排斥锁的问题在于:让线程睡眠和唤醒线程都是极为耗时的操作,完毕这些操作须要大量CPU指令,因此也就须要耗费不少时间。假设仅仅是锁定相互排斥锁非常短一段时间,那么让线程睡眠和唤醒线程所花的时间可能会超过线程实际上睡眠的时间,甚至有可能会超过线程在自旋锁上轮询锁浪费的时间(假设使用自旋锁)。还有一方面,在自旋锁上进行轮询会浪费CPU时间,假设自旋锁被锁定较长的时间,可能会浪费大量的CPU时间,这时让线程睡眠可能是一个更好的选择。
3 解决方法
在一个单核系统中使用自旋锁是行不通的,由于只要自旋锁轮询在堵塞当前CPU,那么就没有其它线程可以执行,既然没有其它线程可以执行,那么该锁也就不会被唤醒,对,我们进入死锁了。最好情况下,自旋锁只浪费那些对系统没有不论什么用处的CPU时间。相反,假设使用相互排斥锁,线程A进入睡眠,那么另外一个线程B就行马上执行,线程B有可能会释放锁,唤醒线程A,使线程A继续执行。
在一个多核系统,假设大量的锁仅仅持有非常短一段时间,那么让线程睡眠和唤醒线程所浪费的时间有可能会极大地减少执行时性能。相反,假设使用自旋锁,线程就有机会利用全然时间片(总是堵塞非常短一段时间,然后马上执行),获得更高的吞吐量。
4 实践
由于大部分情况下,程序猿不能预先知道使用相互排斥锁好还是使用自旋锁好(比如:由于不知道目标系统的CPU核心数量),同一时候操作系统也不知道某个片段的代码是否已经为单核或多核环境优化过,因此大部分系统不严格区分这两种锁。实际上,大部分现代操作系统都提供混合相互排斥锁和混合自旋锁。那么,什么是混合相互排斥锁和混合自旋锁?
在一个多核系统,混合相互排斥锁開始时会表现得像自旋锁。即假设一个线程A不能获取到相互排斥锁,那么线程A不会立即进入睡眠状态,由于该锁可能立即就被释放了,因此该相互排斥锁開始表现得像自旋锁。仅仅有当一段固定的时间后,线程A还不能获取到该相互排斥锁,线程A才会进入睡眠状态。假设同样的程序执行在单核系统下,该相互排斥锁就不会表现出自旋锁的行为。
一个混合自旋锁開始时会表现得像一个普通的自旋锁,但为了避免浪费CPU时间,它提供了一个back-off策略。通常,混合自旋锁不会使线程进入睡眠状态(由于当你使用自旋锁时,你不希望发生这样的情况),可是它能停止某个线程(马上或者一段固定的时间后),然后让还有一个线程执行,以提高自旋锁的闲置率(一个纯粹的线程切换通常比使线程进入睡眠然后唤醒它效率更高,起码眼下如此)。
5 总结
假设你不知道该使用哪一个,那么使用相互排斥锁,由于大部分现代操作系统都同意他们先自旋一小段时间(提前是该自旋故意), 所以相互排斥锁一般是更好的选择。有时,使用自旋锁会提升性能,在某些特定情况下,你可能会认为使用自旋锁更好。这时候,使用你自己的锁对象,该锁对象内部使用自旋锁或者相互排斥锁实现(这个行为能够通过配置改动),開始时所有使用相互排斥锁,之后,假设你认为某个地方使用自旋锁更好,那么改动它,然后比較下结果,可是在下结论之前,一定要记得在单核和多核环境下进行測试。
自旋锁与相互排斥锁之抉择