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++原子库

5 C++原子操作

2 互斥量/互斥锁

互斥器(mutex)恐怕是使用得最多的同步原语,粗略地说,它保护了临界区,任何一个时刻最多只能有一个线程在此mutex划出的临界区内活动。单独使用mutex时,我们主要为了保护共享数据。

2.1 Linux pthread互斥锁

2 Posix线程并发控制

2.2 内核互斥锁

第九十章 内核同步

2.3 C++线程库互斥锁

3 互斥锁

3 自旋锁

2 Posix线程并发控制

4 读写锁

4.1 内核读写自旋锁

第九十章 内核同步

4.2 内核读写顺序锁

第九十章 内核同步

4.3 Linux读写锁

2 Posix线程并发控制

4.4 C++线程库读写锁

3 互斥锁

5 条件变量

5.1 Linux条件变量

2 Posix线程并发控制

5.2 C++线程库条件变量

4 条件变量与future

6 完成量

6.1 Linux完成量

第九十章 内核同步

6.2 C++线程库Future

4 条件变量与future

7 禁止抢占

第九十章 内核同步

8 内存序和屏障

8.1 Linux顺序和屏障

第九十章 内核同步

8.2 C++线程库屏障

C++20开始提供了std::barrier,用于支持屏障操作。

8.3 C++原子库内存序

5 C++原子操作

9 RCU读-复制-更新

6 驱动的并发控制

10 C++并发算法库

10 C++17并行算法