原子操作
原子(atomic)意为不可分割操作,在其他线程视角看来只有已完成或者未完成这两种状态,不存在完成中状态。
它能够确保对共享变量的操作在执行时不会被其他线程的操作干扰,从而避免竞态条件(race condition)和死锁(deadlock)等问题
C++ 标准库提供了<font style="color:rgb(25, 27, 31);">std::atomic<T></font>类模板,可以定义一个任意类型的原子变量。不过根据不同编译器的实现方式,<font style="color:rgb(25, 27, 31);">atomic</font>有时候并不是无锁状态,我们可以通过<font style="color:rgb(25, 27, 31);">is_lock_free</font>这个成员函数来运行时判断这个原子对象是否为无锁结构。
atomic_flag
atomic_flag是C++中最简单的一个原子布尔类型,算是原子对象中最简单的一种类型。
atomic_flag可以通过ATOMIC_FLAG_INIT宏赋值进行初始化,默认是清除状态atomic_flag提供了两个函数来测试和修改标志位,这两个操作都是原子的,可以保证在多线程的环境下正确执行test_and_set返回目前的标志位,并且修改当前的标志位。例如本来是false,调用之后返回false并且自身变成trueclear清除标志位,把自身修改为false
std::atomic_flag 只能表示两种状态,即 true 或 false,不能做其他比较操作。通常情况下,std::atomic_flag 被用作简单的互斥锁(可以用来实现自旋锁),而不是用来存储信息。
atomic
atomic<bool>与atomic_flag颇为相似,前者灵活度较高,接下来介绍一下如何操作。
store
store成员函数实现了将特定的值存储到原子对象中。
定义:
1 | void store(T desired, std::memory_order order = std::memory_order_seq_cst) volatile noexcept; |
desired要存储的值order存储操作的内存顺序。
load
load函数用于获取原子变量的当前值。它有以下两种形式:
1 | T load(memory_order order = memory_order_seq_cst) const noexcept; |
exchange
exchange用于原子变量的交换操作,定义如下:
1 | _TVal exchange(const _TVal _Value, const memory_order _Order = memory_order_seq_cst) noexcept |
返回值为修改之前的值。
compare_exchange_weak和strong
<font style="color:rgb(25, 27, 31);">compare_exchange_weak</font>和<font style="color:rgb(25, 27, 31);">compare_exchange_strong</font>函数作用类似。
如果原子当前值 == expected,就把它改成 desired,并返回 true;否则把 expected 改成“实际读到的当前值”,返回 false。
1 | bool compare_exchange_weak (T& expected, T val,memory_order sync = memory_order_seq_cst) volatile noexcept; |
<font style="color:rgb(25, 27, 31);">expected</font>:期望值的地址,也是输入参数,表示要比较的值;(注意是引用)<font style="color:rgb(25, 27, 31);">val</font>:新值,也是输入参数,表示期望值等于该值时需要替换的值;<font style="color:rgb(25, 27, 31);">success</font>:表示函数执行成功时内存序的类型,默认为<font style="color:rgb(25, 27, 31);">memory_order_seq_cst</font>;<font style="color:rgb(25, 27, 31);">failure</font>:表示函数执行失败时内存序的类型,默认为<font style="color:rgb(25, 27, 31);">memory_order_seq_cst</font>。
该函数的返回值为<font style="color:rgb(25, 27, 31);">bool</font>类型,表示操作是否成功。
注意,<font style="color:rgb(25, 27, 31);">compare_exchange_weak</font>函数是一个弱化版本的原子操作函数,因为在某些平台上它可能会失败(有可能值相等)。如果需要保证严格的原子性,则应该使用<font style="color:rgb(25, 27, 31);">compare_exchange_strong</font>函数。
1 | std::atomic<bool> x{false}; |
atomic<T*>
指向类型 T 的指针,其原子化形式为std::atomic<T*>,类似于原子化的布尔类型 std::atomic<bool>二者的接口相同。但是该原子对象提供了新操作fecth_add和fetch_sub,还有指针运算的符号重载+=、-=还有前后++与–。这些重载符号用起来的效果和内置类型一样。
1 | int arr[5] = { NULL }; |
fecth_add和fetch_sub都是“读-改-写”操作,上面代码中,我们修改了指针指向了数组的第二项,fetch_add(2)会返回原来的指针。这种操作也叫做“交换相加”exchange...
我们可以在fetch_add和fetch_sub函数里指明内存次序,运算符重载的则不能。
atomic
在 std::atomic和 std::atomic这样的整数原子类型上,可以执行的操作颇为齐全:既包括常用的原子操作 load()、store()、exchange()、compare_ exchange_weak() 和 compare_exchange_strong(),也包括原子运算fetch_add()、 fetch_sub()、fetch_and()、fetch_or()、fetch_xor(),以及这些运算的复合赋值形式(+=、 −=、&=、|=和^=),还有前后缀形式的自增和自减(++x、x++、−−x和x−−)。相比普通整型的复合赋值操作,虽然整数原子类型上的相关操作尚不算全面,但已经近乎完整, 所缺少的重载运算符仅仅是乘除与位移。
上述操作的语义非常接近std::atomic类型的fetch_add()和fetch_sub():具名函数按原子化方式执行操作,并返回原子对象的旧值,然而复合赋值运算符则返回新值。
泛化atomic<>类模板
除了上面所说的标准原子类型,我们还可以使用自定义类型U,其原子化类型就是atomic<U>,所提供的接口和atomic<bool>相同。
不过对于类型U,atomic还是有限制的: 必须具备平实拷贝赋值操作符(trivial copy-assignment operator), 它不得含有任何虚函数,也不可以从虚基类派生得出,还必须由编译器代其隐式生成拷贝赋值操符;另外,若自定义类型具有基类或非静态数据成员,则它们同样必须具备 平实拷贝赋值操作符。这样赋值操作才能用memcpy等效完成。
同时需要注意, 比较-交换操作所采用的是逐位比较运算,效果等同于memcmp,假设用户类型自定义了比较运算符,在这项操作里面也会被省略。同时,因为等同于memcmp,如果用户定义的结构体中存在填充位,那么即使类型U的值相等,memcmp的结构却不一定相等,那么“比较-交换(CAS)”操作还是会失败。
1 | struct S { |
- padding 不属于任何成员的值;
- 它们的内容可能是未指定/不稳定的(例如你只给
c、i赋值,并不一定会把 padding 清零)。
对于8字节或者4字节的自定义结构体,编译器往往能够为其生成无锁结构。反之,则会通过互斥锁实现原子操作。
在MSVC源码中我们可以进行观察。对于结构体:
1 | struct myStruct { |
load函数内部其实是使用了自旋锁来实现原子操作。
1 | _NODISCARD _TVal load(const memory_order _Order = memory_order_seq_cst) const noexcept { |
各原子类型上可执行的操作
| 操作 | atomic_flag | atomic |
atomic<T*> | 整数原子类型 | 其他原子类型 |
|---|---|---|---|---|---|
| test_and_set | Y | ||||
| clear | Y | ||||
| is_lock_free | Y | Y | Y | Y | |
| load | Y | Y | Y | Y | |
| store | Y | Y | Y | Y | |
| exchange | Y | Y | Y | Y | |
| compare_exchange_weak, compare_exchange_strong | Y | Y | Y | Y | |
| fetch_add, += | Y | Y | |||
| fetch_sub, -= | Y | Y | |||
| fetch_or, | = | Y | |||
| fetch_and, &= | Y | ||||
| fetch_xor, ^= | Y | ||||
| ++, – | Y | Y |
原子化访问shared_ptr<>
C++标准库还提供了非成员函数,按原子化形式访问std::shared_ptr<>的实例。虽然在原则上,只有原子类型才支持原子操作,std::shared_ptr<>并不属于原子类型。但是C++标准会认为这个很有必要,因此添加了原子操作的非成员函数。在C++20中,直接添加了std::atomic<std::shared_ptr<T>>的特化版本。
std::shared_ptr<>内部的引用计数更新本身是线程安全的,但是对于同一个std::shared_ptr<>被多个线程同时读写时不安全的。C++ 提供了atomic_开头的一系列全局函数,用于操作原子对象,例如atomic_load,其定义有 :
1 | template <class _Ty> |
我们可以发现有shared_ptr<>的重载版本。同时,函数的参数都是指针类型,这个是C++规定,要求函数能够兼容C语言标准。
我们也可以调用带有explicit的函数版本,这个函数版本能够指明内存次序。