自旋锁(spinlock)与互斥锁(mutex)是并发编程中两个重要的概念。它们的主要作用是:对共享资源加锁以阻止数据的并发访问,从而保证数据一致性。但是它们也有一些不同点。本文主要介绍这些不同点,并说明我们什么时候该用自旋锁,什么时候该用互斥锁。
理论基础
理论上,当一个线程尝试去获取一个互斥锁,但由于该互斥锁已经被其它线程获取而没有成功时,它会立刻进入休眠状态,从而让出CPU时间,允许其它线程运行。它将持续休眠直到最终被唤醒,唤醒的条件是之前获取到该互斥锁的线程释放了互斥锁;
对比一下,当一个线程尝试去获取一个自旋锁,但由于该自旋锁已经被其它线程获取而没有成功时, 它将会反复获取它(在英文中叫polling),直到最终成功。因此在获取自旋锁的时候,该线程不会让出CPU时间,其它线程将不能运行,当然,操作系统不会允许一个线程一直阻塞整个系统的运行,在某个线程花完了它的CPU运行总时间后,它会强制切换到另外线程执行。(注:线程一次使用的CPU总时间的最大上限可以通过ulimit -t查看,单位为秒)
问题
对于互斥锁,使线程进入休眠以及唤醒线程都是比较昂贵的操作,需要相当多的CPU指令,花费较长的CPU时间。如果A线程获取到该互斥锁后,只是持有了很短的一段时间就释放,那么B线程在获取互斥锁的过程中,B线程进入休眠以及被唤醒花费的CPU时间可能超过了B线程休眠的时间,甚至可能超过了采用的自旋锁时polling的时间;
另一方面,对于自旋锁,反复获取自旋锁将会花费很多CPU时间,如果A线程持有该锁的时间过长,那么尝试获取该锁的B线程将会占用很多CPU时间,在这种情况下,让B线程进入休眠反而会更好些。
解决方案
不要在单CPU单核的系统上使用自旋锁,因为当A线程获取自旋锁polling的时候,可能会阻塞仅有的CPU导致其它所有的线程都不能运行,而其它线程不能运行意味着需要释放该自旋锁的B线程也得不到CPU时间不能运行,那么该自旋锁将不会被释放。也就是说,自旋锁浪费了仅有的系统CPU时间而没有获得任何好处(能获取到锁的条件是polling耗费掉其CPU总时间后,系统切换到B线程,B线程释放锁后,A线程再次执行时才获得锁)。如果A线程在不能获取到锁的时候就立刻进入休眠,B线程可能就可以立刻执行,有可能B线程会立刻释放掉锁,在A线程被唤醒后,就可以继续执行。
在多CPU多核的系统上,如果大量的锁被持有的时间都很短,那么浪费在不断使线程进入休眠和唤醒线程的时间将会很多,有可能显著的降低系统性能。当采用自旋锁的时候,线程可以有机会利用它们可以使用的CPU时间片,在短暂的阻塞后立刻进行后续的工作,从而最大限度的提高CPU的使用率。
实践策略
因为在多数情况下程序员不知道到底是使用自旋锁还是互斥锁更好(因为不能提前知道程序运行的系统CPU核数),操作系统也不可能知道某段代码被优化为运行在单核或多核环境下,大多数操作系统也并不能严格区分互斥锁还是自旋锁。事实上,多数现代操作系统均采用混合的互斥锁和混合的自旋锁,这是为什么呢?
混合的互斥锁的行为在开始时类似于在多核系统上的自旋锁:如果A线程不能获取锁,它不会被立刻休眠,因为该互斥锁可能会立刻被释放,所以互斥锁开始时表现会类似于自旋锁。只有在A线程等待了一段足够长的时间后还没有获得该互斥锁时才会被休眠。如果同样的程序运行在一个单核系统上,那么互斥锁将不会自旋,因为这没有任何好处。
混合的自旋锁的行为在开始时和一般的自旋锁一样,但是为了避免浪费过多的CPU时间,它可能会采用一个折中的策略:系统不会让线程进入休眠(因为使用自旋锁时你肯定不希望休眠发生),但是会决定什么时候停止线程让其他线程运行(要么立刻,要么在一段固定时间后),因此增加了自旋锁解锁的机率。(单纯的线程切换通常没有使线程进入休眠再唤醒线程代价那么昂贵)
总结
如果不确定,那么使用互斥锁,这通常是更好的选择。在能够获得益处的情形下,大多数的现代操作系统将允许互斥锁自旋(polling)一段时间。采用自旋锁在某些时候会提高性能,但是只是在某些特定的条件下。事实上,你是在怀疑而不是告诉我,你目前没有任何项目采用自旋锁可能获益。你可能会采用你自己定义的“锁对象”,它在内部要么使用自旋锁,要么使用互斥锁(例如,使用何种类型的锁在创建时是可配置的),初始时,在所有的地方都使用互斥锁,如果在某些地方使用自旋锁可能会有帮助,那么请试一试,并比较二者的结果,在得出结论前,一定要确保比较的时候要测试了单核和多核系统,如果你的程序是跨平台的,也请测试各种操作系统。
原文:http://www.pixelstech.net/article/1397962421-Practice-of-using-spinlock-instead-of-mutex
自旋锁与互斥锁