1. 生活中的例子
互斥锁vs自旋锁 ------ 门口排队vs不停敲门
可以代入一下,假设你去公司上厕所,发现厕所门是锁着的:
互斥锁(Mutex)的做法:
- 你看到有人在里面,就去门口的椅子上坐着睡觉
- 操作系统会把你这个"人(线程)"从CPU上移走,让CPU去做别的事情
- 等里面的人出来了会叫醒你,你再进去
自旋锁(Spinlock)的做法
- 你看到有人在里面,你不睡觉,就是在门口不停地敲门
- 你一直占用着CPU,什么也不做,就循环检查"门开了没有"
- 只要门一开,你就冲进去
2. 主要区别
| 特性 | 互斥锁(Mutex) | 自旋锁(Spinlock) |
|---|---|---|
| 等待方式 | 阻塞等待,线程休眠,让出CPU | 忙着等,线程一直运行,占着CPU |
| 上下文切换 | 有,线程休眠和唤醒都需要切换 | 无,一直运行在CPU上 |
| 性能开销 | 等待时间长时开销小 | 等待时间短时开销小 |
| 死锁风险 | 高(会导致线程永久休眠) | 更高(会导致CPU占用100%) |
3. 为什么会有这两种锁同时存在 ?
互斥锁的优缺点
- 优点:等待时不占用CPU,适合长时间等待
- 缺点:有上下文切换的开销,这个开销还是挺大的(大约几千个CPU周期)
自旋锁的优缺点
- 优点:无上下文切换的开销,等待时间短时速度极快
- 缺点:等待期间一直占用CPU,等待时间长了会浪费大量CPU资源
4. 怎么实现C++中的自旋锁 ?
C++标准库直到C++20才提供了官方的自旋锁 (std::spinlock)
- 在C++11/C++14中,我们可以用
std::atomic_flag自己实现一个自旋锁
cpp
#include <atomic>
//C++11就能用的自旋锁实现
class Spinlock
{
private:
//std::atomic_flag是唯一保证无锁的原子类型
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock()
{
// 循环尝试设置标记位直到成功
// test_and_set() : 设置为true,返回之前的值
while (flag.test_and_set(std::memory_order_acquire))
{
//什么也不做,一直占用CPU,这就是"自旋"
}
}
void unlock()
{
//清除标记位
flag.clear(std::memory_order_release);
}
};