1 原子操作
1.1 为什么需要原子操作?
i++问题
在多线程编程中,最常拿来举例的问题便是著名的i++问题,即:多个线程对同一个共享变量i执行i++ 操作。这样做之所以会出现问题的原因在于i++可以分为三个汇编步骤:
step | operation |
---|---|
1 | i->reg(读取i的值到寄存器) |
2 | inc-reg(在寄存器中自增i的值) |
3 | reg->i (写回内存中的i) |
上面三个步骤中间是可以间隔的,并非原子操作,也就是说多个线程同时执行的时候可能出步骤的交叉执行,例如下面的情况:
- 期望结果:两个线程分别i++,结果为2
- 实际结果:指令顺序问题,结果为1 | step | thread A | thread B | | —- | —- | —- | | 1 | i->reg | | | 2 | inc-reg | | | 3 | | i->reg | | 4 | | inc-reg | | 5 | reg->i | | | 6 | | reg->i |
指令重排问题
有时候,我们会用一个变量作为标志位,当这个变量等于某个特定值的时候就进行某些操作。但是这样依然可能会有一些意想不到的坑,例如两个线程以如下顺序执行:当B判断flag为true后,断言a为1
step | thread A | thread B |
---|---|---|
1 | a = 1 | |
2 | flag= true | |
3 | if flag== true | |
4 | assert(a == 1) |
那么一定是这样吗?可能不是,因为编译器和CPU都可能将指令进行重排(编译器不同等级的优化和CPU的乱序执行)。这种重排有可能会导致一个线程内相互之间不存在依赖关系的指令交换执行顺序,以获得更高的执行效率。实际上的执行顺序可能变成这样:
- 期望结果:断言通过
- 实际结果:断言失败 | step | thread A | thread B | | —- | —- | —- | | 1 | flag = true | | | 2 | | if flag== true | | 3 | | assert(a == 1) | | 4 | a = 1 | |
更详细解释参考:https://www.yuque.com/barret/giv6pv/mt7u7n#is1Io
1.2 Linux原子操作
1.3 C++原子库
2 互斥量/互斥锁
互斥器(mutex)恐怕是使用得最多的同步原语,粗略地说,它保护了临界区,任何一个时刻最多只能有一个线程在此mutex划出的临界区内活动。单独使用mutex时,我们主要为了保护共享数据。
2.1 Linux pthread互斥锁
2.2 内核互斥锁
2.3 C++线程库互斥锁
3 自旋锁
4 读写锁
4.1 内核读写自旋锁
4.2 内核读写顺序锁
4.3 Linux读写锁
4.4 C++线程库读写锁
5 条件变量
5.1 Linux条件变量
5.2 C++线程库条件变量
6 完成量
6.1 Linux完成量
6.2 C++线程库Future
7 禁止抢占
8 内存序和屏障
8.1 Linux顺序和屏障
8.2 C++线程库屏障
C++20开始提供了std::barrier
,用于支持屏障操作。