C++之路(2)atomic
从C++11标准开始,C++开始支持原子量与原子操作。C++20增加 wait
功能。
#include <atomic>
一、原子量
std::atomic_bool
即为std::atomic<bool>
std::atomic_int
即为std::atomic<int>
std::atomic_uint
即为std::atomic<unsigned int>
std::atomic_flag
,不同于atomic<>
,它保证是lock-free的。比std::atomic<bool>
速度快,不同于std::atomic<bool>
,它不提供load
和store
操作;而是提供test
、test_and_set
和clear
操作。
C++20另支持 std::atomic<std::shared_ptr>
和 std::atomic<std::weak_ptr>
注意: 所有 std::atomic
都不支持 copyable
和 movable
。
二、原子量的操作
store
赋值load
取值exchange
赋新值并取原值fetch_add
加上fetch_sub
减去compare_exchange_weak
/compare_exchange_strong
与第一个参数比较:如果相等,用第二个参数赋新值,返回true
;如果不相等,取值并赋给第一个参数,返回false
。weak
版本可能无故地fail(fail spuriously),但速度快!使用时外层需要有循环(while
)保护,防止无故fail。strong
版本:如果仅仅为了防止无故fail而将weak版本放入循环,则不如直接使用strong
版本。
三、原子量操作的重要参数:memory_order
1. 理解 memory order 的背景知识
代码书写顺序不代表运行顺序。书写一句代码编译后往往被分解成多句汇编及机器语言。
编译器优化经常把不相干的语句调整顺序;CPU运行优化时也经常将不相干的语句并行运行。但优化绝对不改变对相同变量的关联性操作的顺序逻辑,因此在单线程中,这些优化都不会造成任何问题。感觉上,运行顺序与书写顺序一致。
而在多线程编程中,编译器对语句顺序的优化调整和CPU内部的并行运算,相互交叉,将会造成超出每个线程书写顺序理解的诡异结果。
最简单的解决多线程上述问题的方法是用 mutex
等的锁;而想追求极致速度,可使用无锁编程技术。无锁编程的基础就是深刻理解运用 memory order。
memory order 是约束单一线程中原子操作语句前后不相干语句的执行顺序能够以什么程度被重新排列优化。
2. memory order 参数
typedef enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
} memory_order;
memory_order_relaxed
:最宽松(无)约束。只保证本操作的原子性,对其他读写操作没有同步和顺序限制。(遵从最原始的优化原则,同一线程对变量的相关操作被约束顺序,不相关操作可能被无约束改变顺序进行优化)memory_order_consume
:取值操作,保证这句在最前,即当前线程没有其他相关读写操作被优化到本次读值之前。(目前C++标准不鼓励使用)memory_order_acquire
:取值操作,保证这句在最前,即当前线程没有其他任何读写操作被优化到本次读值之前。(例如:mutex的lock操作)memory_order_release
:赋值操作,保证这句在最后,即当前线程没有其他任何读写操作被优化到本次赋值之后。(例如:mutex的unlock操作)memory_order_acq_rel
:取值并改写的操作,相当于acquire
和release
。memory_order_seq_cst
:取值操作,相当于acquire
;赋值操作,相当于release
;取值并改写的操作,相当于acq_rel
;并且附加更严格的控制顺序,保证所有线程所有修改按照同样的顺序。(C++的默认)
注意: C++原子操作默认 order 的参数都用 memory_order_seq_cst
即满足 sequentially consistent ordering
,牺牲了性能。
3. 多线程中 memory order 的使用
Sequentially-consistent ordering
:赋值、取值用seq_cst
。最强的顺序约束。牺牲性能最大。(C++的默认)Release-Acquire ordering
:一个线程赋值用release
;另一个线程取值用acquire
。顺序强约束。(x86架构机器绝大多数的操作支持这个,不支持更松的约束了)Release-Consume ordering
:一个线程赋值用release
;另一个线程取值用consume
。很少修改但经常读取的数据结构可以用这个。(目前C++标准不鼓励用)Relaxed ordering
:赋值、取值用relaxed
。最松约束、最优性能。(arm架构机器支持,x86架构机器不支持)
4. 总结
C++原子操作的默认
Sequentially-consistent ordering
约束太强,牺牲性能。
建议显性使用Release-Acquire ordering
,即store
用release
;load
用acquire
。
四、原子量的 wait/notify
C++20后,原子量开始支持 wait
、notify_one
、notify_all
。
通常比自旋锁spinlock效率高。
void wait( T old, std::memory_order order =
std::memory_order::seq_cst);
void notify_one();
- 调用时,判断
值
等于old
,进入wait;值
不等于old
,不wait,直接return。 notify_one
、notify_all
唤醒,也可能被无故唤醒(spuriously)。- 被唤醒时,仅当
值
不等于old
时,才return;如相等,继续wait。 - 调用时建议使用
std::memory_order::acquire
。
最后修改于 2024-02-24