原子操作

原子(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并且自身变成true
    • clear清除标志位,把自身修改为false

std::atomic_flag 只能表示两种状态,即 true 或 false,不能做其他比较操作。通常情况下,std::atomic_flag 被用作简单的互斥锁(可以用来实现自旋锁),而不是用来存储信息。

atomic

atomic<bool>atomic_flag颇为相似,前者灵活度较高,接下来介绍一下如何操作。

store

store成员函数实现了将特定的值存储到原子对象中。

定义:

1
2
void store(T desired, std::memory_order order = std::memory_order_seq_cst) volatile noexcept;
void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;
  • desired要存储的值
  • order存储操作的内存顺序。

load

load函数用于获取原子变量的当前值。它有以下两种形式:

1
2
T load(memory_order order = memory_order_seq_cst) const noexcept;
operator T() 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
2
3
4
bool compare_exchange_weak (T& expected, T val,memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak (T& expected, T val,memory_order sync = memory_order_seq_cst) noexcept;
bool compare_exchange_weak (T& expected, T val,memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_weak (T& expected, T val,memory_order success, memory_order failure) 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
2
3
4
5
6
7
8
9
std::atomic<bool> x{false};

bool old = x.load(std::memory_order_relaxed);
while (!x.compare_exchange_weak(old, !old,
std::memory_order_release,
std::memory_order_relaxed)) {
// 失败时:old 自动被改成当前 x
// 循环里用 !old 再试
}

atomic<T*>

指向类型 T 的指针,其原子化形式为std::atomic<T*>,类似于原子化的布尔类型 std::atomic<bool>二者的接口相同。但是该原子对象提供了新操作fecth_addfetch_sub,还有指针运算的符号重载+=、-=还有前后++与–。这些重载符号用起来的效果和内置类型一样。

1
2
3
4
5
6
int arr[5] = { NULL };
atomic<int*> pointer;

pointer.store(arr);
pointer.fetch_add(2); //arr[2]
*pointer = 12;

fecth_addfetch_sub都是“读-改-写”操作,上面代码中,我们修改了指针指向了数组的第二项,fetch_add(2)会返回原来的指针。这种操作也叫做“交换相加”exchange...

我们可以在fetch_addfetch_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
2
3
4
5
6
struct S {
char c; // 1 字节
int i; // 通常需要 4 字节对齐
};
// 在很多平台上:sizeof(S) == 8
// c 占 1 字节 + 后面有 3 字节 padding + i 占 4 字节
  • padding 不属于任何成员的值
  • 它们的内容可能是未指定/不稳定的(例如你只给 ci 赋值,并不一定会把 padding 清零)。

对于8字节或者4字节的自定义结构体,编译器往往能够为其生成无锁结构。反之,则会通过互斥锁实现原子操作。

在MSVC源码中我们可以进行观察。对于结构体:

1
2
3
4
5
6
7
struct myStruct {
int a;
int b;
int c;
};
atomic<myStruct> dbat; //12字节
dbat.exchange({});

load函数内部其实是使用了自旋锁来实现原子操作。

1
2
3
4
5
6
7
_NODISCARD _TVal load(const memory_order _Order = memory_order_seq_cst) const noexcept {
// load with sequential consistency
_Check_load_memory_order(_Order);
_Guard _Lock{_Spinlock}; //自旋互斥锁
_TVal _Local(_Storage);
return _Local;
}

各原子类型上可执行的操作

操作 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
2
3
4
5
6
template <class _Ty>
shared_ptr<_Ty> atomic_load(const shared_ptr<_Ty>* _Ptr) ;
template <class _Ty>
_Ty atomic_load(const atomic<_Ty>* const _Mem) noexcept;
template <class _Ty>
_Ty atomic_load(const volatile atomic<_Ty>* const _Mem) noexcept;

我们可以发现有shared_ptr<>的重载版本。同时,函数的参数都是指针类型,这个是C++规定,要求函数能够兼容C语言标准。

我们也可以调用带有explicit的函数版本,这个函数版本能够指明内存次序。