并发编程中, 当多线程访问共享资源时, 会产生竞争条件(race condition).
1. 测试
Base作为抽象基类, 提供bench()测试接口: 启动两个线程, 一个线程对共享资源m_var执行加一操作, 另一个线程对m_var执行减一操作. 正常情况下, 最后m_var预期结果应为0
1 | class Base { |
Base基类派生出4个派生类, 对应4种不同的实现方案
- Derived: 对共享资源不加任何保护
- DerivedVolatile: 对共享资源添加volatile关键字修饰
- DerivedMutex: 对共享资源添加std::mutex互斥访问
- DerivedAtomic: 将共享资源设置为原子类型
1 | graph BT |
1.1 测试环境
- Linux ubuntu18arm64 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:25:58 UTC 2020 aarch64 aarch64 aarch64 GNU/Linux
- gcc version 7.4.0 (Ubuntu/Linaro 7.4.0-1ubuntu1~18.04.1)
- C++11
1.2 测试结果
Debug版本
1 | $ ./out/bin/atomic |
Release版本
1 | $ ./out/bin/atomic |
2. 实现方案
2.1 不加保护
测试代码
1 | class Derived : public Base { |
Release版本的汇编代码如下
1 | 00000000000027d8 <_ZN7Derived5iplusEi>: |
从两个函数的汇编代码来看
- 由于iplus()/iminus()逻辑简单, 编译器在Release版本直接将while循环优化掉, 测试结果没有报错.
- 从Debug版本测试结果来看, 多线程访问共享资源是有问题的.
2.2 volatile
该版本中, 在共享资源m_var前添加volatile关键字进行修饰.
测试代码
1 | class DerivedVolatile : public Base { |
反汇编如下
1 | 0000000000002808 <_ZN15DerivedVolatile5iplusEi>: |
从两个函数的汇编代码来看
- 加了volatile关键字后, 编译器没有再进行激进的优化, 而是按照程序正常流程进行编译.
- Debug/Release版本测试结果都显示多线程计算结果错误
- volatile只能防止编译器做优化, 不能保证原子性.
2.3 std::mutex
该版本中, 对共享资源m_var添加std::mutex互斥访问.
测试代码
1 | class DerivedMutex : public Base { |
反汇编如下
1 | 0000000000002880 <_ZN12DerivedMutex6iminusEi>: |
从两个函数的汇编代码来看
- std::mutex对m_var互斥访问, 底层调用pthread_mutex_lock()/pthread_mutex_unlock()来实现.
- Debug/Release版本测试结果显示多线程计算结果符合预期
2.4 std::atomic
该版本, 直接将共享资源int m_var改为原子类型std::atomic
测试代码
1 |
|
反汇编如下
1 | 0000000000002918 <_ZN13DerivedAtomic6iminusEi>: |
从两个函数的汇编代码来看
- 以iplus()函数为例,
m_var++操作, 对应RMW(Read-Modify-Write), ARM硬件平台默认(顺序一致性内存模型)使用ldaxr/stlxr原子指令保证原子操作, 这里写回的实现有点类似spinlock - Debug/Release版本测试结果显示多线程计算结果符合预期
3. 总结
实现方案 | 正确性 | 说明 |
---|---|---|
不加保护 | 无法保证程序的正确性 | 多线程访问共享资源会产生竞争条件 |
volatile | 无法保证程序的正确性 | volatile只能防止编译器进行优化, 无法保证原子性 |
std::mutex | 能保证程序的正确性 | 锁的粒度大, 影响性能 |
std::atomic | 能保证程序的正确性 | 性能较好 |