第五章 C++ 内存模型与原子操作
第五章 C++ 内存模型与原子操作
多线程感知模型(multithreading-aware memory model)是C++11最重要的特性特性之一, 以下介绍它的要点信息:
- Synchronization Primitives:同步原语
- mutex 和 condition variable 等同步原语可以用来保护数据和信号事件(signal events).
- Atomic Operations and Fences:原子操作和栅栏
- 内存模型定义的两种新的可移植方法来同步多线程环境中的内存访问.
- 可使用 atomic load/store 在指定内存位置进行读写操作。
- Atomic Types & Operations:原子类型和操作
- C++ 标准库提供原子类型
- 这些类型上可用的操作,以及如何使用这些操作在线程之间提供同步.
- C++ Tutorial => C++11 Memory Model (riptutorial.com)
- Semantics of Memory Access: 内存访问语义
- C++11内存模型对内存访问的语义做出了最小的保证.
- 它限制了优化对执行语义的潜在影响,并讨论了程序员控制语义某些方面的技术,使得我们能够确保代码的正确性
Memory model basics
C++中的所有数据都由对象组成,这里的"对象"仅仅是对数据构建块的声明。有四个重要的原则需要记住:
- 每个变量都是一个对象,包括类的成员变量对象。
- 每个对象至少占用一个内存位置。
- 内置基本类型(如int或char)都有确定的内存位置,无论其大小如何。
- 相邻位域(adjacent bit filelds)共享同一个内存位置。
当两个线程同时访问同一内存区域,并且没有强制规定访问的顺序时,可能会出现问题。
特别是,如果其中一个或多个访问是非原子化的,并且涉及到写操作,那么就可能出现数据竞争,从而导致未定义的行为。
在此情境下,如果我们采用原子操作来处理所有的访问,虽然不能完全避免数据竞争,但可以确保不会出现未定义行为。
- 考虑两个线程同时对一个原子变量进行递增操作。虽然每个递增操作本身是原子的,但是两个线程的操作之间仍然存在竞争关系。
所有线程对某个对象的所有修改操作,形成该对象的修改序列。如果使用原子操作,编译器有责任确保有效的同步,这样做的目的是确保对于任何对象,所有线程对其进行的修改序列是一致的。
Atomic operations and types in C++
在多线程编程中,**原子类型(Atomic Types)**是指那些在多个线程之间共享,并且可以安全地执行读写操作的数据类型。
原子类型的操作在执行时不会被其他线程的操作干扰,从而避免了数据竞争(Race Condition)。
原子操作是指在多线程环境中,不会被其他线程打断的操作。这意味着,一个原子操作要么全部完成,要么全部未完成,不会出现只完成了一部分的情况。
因此,原子操作可以在没有使用互斥锁(Mutex)或者其他同步机制的情况下,安全地在多个线程之间共享。
Info
Mutex Vs Atomic
互斥量(Mutex):
- 互斥量主要用于保护共享资源,防止多个线程同时访问,从而避免数据竞争和不一致。
- 互斥量的保护颗粒度较大,可以保护一个代码块,一个函数,甚至一个对象。
- 使用互斥量时,需要注意避免死锁。
原子操作(Atomic Operations):
- 原子操作主要用于实现无锁数据结构和算法。
- 原子操作的保护颗粒度较小,通常只能保护一个变量。
- 原子操作通常比互斥量更快,因为它们不需要进行上下文切换。
使用atomic 并不能保证无锁的实现,它只是无锁实现的一种便捷选择,是否真的是无锁,还需要通过is_lock_free去判断。
stdmemory_order
在C++中,在<atomic>定义了std::atomic模板,用于定义原子类型。https://zh.cppreference.com/w/cpp/atomic/atomic
std::atomic_flag
stdatomic的特化版本,也是最简单的标准原子类型。 https://zh.cppreference.com/w/cpp/atomic/atomic_flag
- C++中的一个原子布尔类型,保证是无锁的(lock-free), 其对象必须由ATOMIC_FLAG_INIT初始化并置零.
- 与 stdatomic_flag 不提供加载或存储操作, 只支持clear、test_and_set。
- 用于实现原子锁操作。
spinlock sample
#include <atomic>
#include <thread>
#include <vector>
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void f(int n) {
for (int cnt = 0; cnt < 100; ++cnt) {
while (lock.test_and_set(std::memory_order_acquire)); // acquire lock
std::cout << "Output from thread " << n << '\n';
lock.clear(std::memory_order_release); // release lock
}
}
int main() {
std::vector<std::thread> v(10);
for (int n = 0; n < 10; ++n) {
v[n] = std::thread(f, n);
}
for (auto& t : v) {
t.join();
}
}
:::success
自旋锁(spinlock)是一种用于多线程同步的锁机制。
- 自旋锁的定义:
- 当一个线程尝试获取某把锁时,如果该锁已经被其他线程持有(占用),那么该线程会循环等待,不断地检查锁是否能够成功获取,直到获取到锁才会退出循环。
- 自旋锁不会将线程阻塞起来,而是在循环中等待,不断尝试获取锁。这种等待方式被称为自旋等待。
- 自旋锁的特点:
- 忙等待:自旋锁会让线程忙等待,不断检查锁的状态,直到获取到锁为止。
- 适用场景:自旋锁适用于临界区代码执行时间短暂、线程等待时间短的情况。
- 节省CPU资源:相比于阻塞式的锁,自旋锁不会让线程进入休眠状态,因此节省了CPU资源。
:::
std::atomic<bool>
- C++中的一个原子布尔类型,支持比std::atomic_flag更全面。包含Load, Store, Exchange等。
- 它可能不是lock-free实现的。
- 需要注意的是就算基础bool类型的读写操作是原子的,也不能保证内存访问顺序性,因此无法确保访问的一致性。
所以在多线程环境,使用std::stomic<bool>才是恰当的选择。
std::memory_order
https://zh.cppreference.com/w/cpp/atomic/memory_order
现代的CPU和编译器可能会对指令进行重排,以提高执行效率。然而,这种重排可能会导致在多线程环境下出现问题。
例如,两个线程可能会同时读取和写入同一块内存,如果这些操作的顺序发生了改变,可能会导致不可预期的结果。这就是所谓的数据竞争。
每个原子操作都需要指定一个内存顺序 (memory order). 不同的内存顺序有不同的语义, 会实现不同的顺序模型 (order model), 性能也各不相同.
std::memory_order在C++多线程开发中的主要用途是控制和约束内存操作的顺序,以避免数据竞争和其他多线程相关的问题。
- 指定内存访问,包括常规的非原子内存访问,如何围绕原子操作排序.
- 更多的细节会在下面的章节解释.
std::atomic 的成员函数
构造函数
https://zh.cppreference.com/w/cpp/atomic/atomic/atomic
is_lock_free
https://zh.cppreference.com/w/cpp/atomic/atomic/is_lock_free
is_lock_free用于检查当前的atomic对象是否支持无锁操作。
- is_lock_free返回true,那么它的原子操作是无锁的。
- 无锁的原子操作通常是硬件指令来实现的。这些硬件指令可以直接操作内存,而无需使用互斥锁。
- 并不是所有的stdatomic类型不是lock_free,那么它可能需要使用互斥锁或者其他的锁定操作来保证操作的原子性。
这个锁定操作是由std::atomic的实现自动完成的,而不需要我们显式地添加一个互斥锁。
store & load
- store用于把给定的值存储到原子对象中,需要注意参数2使用默认的 memory_order_seq_cst
- load用于获取原子变量的当前值,建议参数使用默认的 memory_order_seq_cst
- 需要注意store和load的参数2都和内存顺序有关,如果使用出错会导致未定义行为。
Prototype
void store( T desired, std::memory_order order = std::memory_order_seq_cst );
T load( std::memory_order order = std::memory_order_seq_cst );
operator T() const ; // 原子地加载并返回原子变量的当前值。等价于 load()
exchange & compare_exchange_weak & compare_exchange_strong
exchange
- 以原子方式将把底层值替换为所需的值, 并返回原子变量在调用前的值。
- 操作为读-修改-写操作。
- 根据内存顺序 order 的值影响内存结果。
- https://zh.cppreference.com/w/cpp/atomic/atomic/exchange
compare_exchange_weak & compare_exchange_strong
- 两个函数的作用都是是比较一个值和一个期望值是否相等,并且在相等时将该值替换成一个新值。
- compare_exchange_strong会保证原子性,并且如果比较失败则会返回当前值。
- compare_exchange_weak函数是一个弱化版本的原子操作函数,因为在某些平台上它可能会失败并重试。
- https://zh.cppreference.com/w/cpp/atomic/atomic/compare_exchange
Prototype
T exchange( T desired, std::memory_order order = std::memory_order_seq_cst );
bool compare_exchange_weak( T& expected, T desired, std::memory_order order = std::memory_order_seq_cst );
bool compare_exchange_strong( T& expected, T desired, std::memory_order order = std::memory_order_seq_cst );
Compare-and-exchange operations are often used as basic building blocks of lock-free data structures.
#include <atomic>
template<typename T>
struct node
{
T data;
node* next;
node(const T& data) : data(data), next(nullptr) {}
};
template<typename T>
class stack
{
std::atomic<node<T>*> head;
public:
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
// 将 head 的当前值放到 new_node->next 中
new_node->next = head.load(std::memory_order_relaxed);
// 现在令 new_node 为新的 head ,但如果 head 不再是
// 存储于 new_node->next 的值(某些其他线程必须在刚才插入结点)
// 那么将新的 head 放到 new_node->next 中并再尝试
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed))
; // 循环体为空
// 注意:上述使用至少在这些版本不是线程安全的
// 先于 4.8.3 的 GCC(漏洞 60272),先于 2014-05-05 的 clang(漏洞 18899)
// 先于 2014-03-17 的 MSVC(漏洞 819819)。下面是变通方法:
// node<T>* old_head = head.load(std::memory_order_relaxed);
// do
// {
// new_node->next = old_head;
// }
// while (!head.compare_exchange_weak(old_head, new_node,
// std::memory_order_release,
// std::memory_order_relaxed));
}
};
int main()
{
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
}
Info
原子变量支持的基本操作有:
- 加法:a += n或者a.fetch_add(n)
- 减法:a -= n或者a.fetch_sub(n)
- 与、或、异或运算:a &= b、a |= b、a ^= b或者a.fetch_and(b)、a.fetch_or(b)、a.fetch_xor(b)
- 自增、自减运算:++a、--a、a++、a--或者a.fetch_add(1)、a.fetch_sub(1)
- 交换:a.exchange(b)返回原来的值,将a设置为b
- 比较并交换:a.compare_exchange_strong(b, c)或者a.compare_exchange_weak(b, c),如果a的值等于b,则将a设置为c,返回true,否则返回false。
atomic operation library
https://zh.cppreference.com/w/cpp/atomic
原子库为细粒度的原子操作提供组件,允许无锁并发编程。涉及同一对象的每个原子操作,相对于任何其他原子操作是不可分的。
这个库提供了一些用于原子类型的操作,比如: atomic_is_lock_free, atomic_store、atomic_load 等。
这个库的存在意义在于,它提供了一种细粒度的原子操作,允许进行无锁并发编程。这对于需要高效并发控制的场景非常有用。
此外,std::atomic模板的特化并没有实现所有的接口函数,这样可以通过原子操作库来提供一些支持。比如,你可以通过atomic_is_lock_free函数来判断atomic_flag是否无锁。
Synchronizing operations and enforcing ordering
修改顺序 (Modification orders)
对一个原子变量的所有修改操作总是存在一定的先后顺序, 且所有线程都认可这个顺序, 即使这些修改操作是在不同的线程中执行的. 这个所有线程一致同意的顺序就称为修改顺序 (modification order). 这意味着:
- 两个修改操作不可能同时进行, 一定存在一个先后顺序. 这很容易理解, 因为这是原子操作必须保证的, 否则就有数据竞争的问题.
- 即使每次运行的修改顺序可能都不同, 但所有线程看到的修改顺序总是一致的. 如果线程 a 看到原子变量 x 由 1 变成 2, 那么线程 b 就不可能看到 x 由 2 变成 1.
无论使用哪种内存顺序, 原子变量的操作总能满足修改顺序一致性, 即使是最松散的 memory_order_relaxed. 我们来看一个例子
sample
std::atomic<int> a{0};
void thread1() {
for (int i = 0; i < 10; i += 2)
a.store(i, std::memory_order_relaxed);
}
void thread2() {
for (int i = 1; i < 10; i += 2)
a.store(i, std::memory_order_relaxed);
}
void thread3(vector<int> *v) {
for (int i = 0; i < 10; ++i)
v->push_back(a.load(std::memory_order_relaxed));
}
void thread4(vector<int> *v) {
for (int i = 0; i < 10; ++i)
v->push_back(a.load(std::memory_order_relaxed));
}
int main() {
vector<int> v3, v4;
std::thread t1(thread1), t2(thread2), t3(thread3, &v3), t4(thread4, &v4);
t1.join(), t2.join(), t3.join(), t4.join();
for (int i : v3) cout << i << " ";
cout << endl;
for (int i : v4) cout << i << " ";
cout << endl;
return 0;
}
// 上面的代码创建了 4 个线程. thread1 和 thread2 分别将偶数和奇数依次写入原子变量 a, thread3 和 thread4 则读取它们.
// 最后输出 thread3 和 thread4 每次读取到的值. 程序运行的结果可能是这样的:
// first test
1 8 7 7 7 9 9 9 9 9
0 2 8 8 8 7 9 9 9 9
// second test
1 2 5 6 9 9 9 8 8 8
1 3 2 5 9 8 8 8 8 8
// 虽然每次运行的修改顺序不同, 各个线程也不太可能看到每次修改的结果, 但是它们看到的修改顺序是一致的.
// 例如 thread3 看到 8 先于 9, thread4 也会看到 8 先于 9, 反之亦然.
happens-before
- Happens-before 是一个非常重要的概念.
如果操作A happens-before 操作B,那么操作A的结果对操作B是可见的。 - happens-before 的关系可以建立在同一个线程的两个操作之间, 也可以建立在不同的线程的两个操作之间.
单线程的情况: sequenced-before
单线程的情况很容易理解. 函数的语句按顺序依次执行, 前面的语句先执行, 后面的后执行. 正式地说, 前面的语句总是 “sequenced-before” 后面的语句. 显然, 根据定义, sequenced-before 具有传递性:
- 如果操作 a “sequenced-before” 操作 k, 且操作 k “sequenced-before” 操作 b, 则操作 a “sequenced-before” 操作 b.
Sequenced-before 可以直接构成 happens-before 的关系. 如果操作 a “sequenced-before” 操作 b, 则操作 a “happens-before” 操作 b. 例如
sample
a = 42; // (1)
cout << a << endl; // (2)
// 语句 (1) 在语句 (2) 的前面, 因此语句 (1) “sequenced-before” 语句 (2), 也就是 (1) “happens-before” 语句 (2).
// 所以 (2) 可以打印出 (1) 赋值的结果.
多线程的情况: synchronizes-with 和 inter-thread happens-before
多线程的情况就稍微复杂些. 一般来说多线程都是并发执行的, 如果没有正确的同步操作, 就无法保证两个操作之间有 happens-before 的关系.
如果我们通过一些手段, 让不同线程的两个操作同步, 我们称这两个操作之间有 synchronizes-with 的关系.
稍后我们会详细讨论如何组合使用 C++ 6 种内存顺序, 让两个操作达成 synchronizes-with 的关系.
如果线程 1 中的操作 a “synchronizes-with” 线程 2 中的操作 b, 则操作 a “inter-thread happens-before” 操作 b.
此外 synchronizes-with 还可以 “后接” 一个 sequenced-before 关系组合成 inter-thread happens-before 的关系:
- 如果操作 a “synchronizes-with” 操作 k, 且操作 k “sequenced-before” 操作 b, 则操作 a “inter-thread happens-before” 操作 b.
Inter-thread happens-before 关系则可以 “前接” 一个 sequenced-before 关系以延伸它的范围; 而且 inter-thread happens-before 关系具有传递性:
- 如果操作 a “sequenced-before” 操作 k, 且操作 k “inter-thread happens-before” 操作 b, 则操作 a “inter-thread happens-before” 操作 b.
- 如果操作 a “inter-thread happens-before” 操作 k, 且操作 k “inter-thread happens-before” 操作 b, 则操作 a “inter-thread happens-before” 操作 b.
正如它的名字暗示的, 如果操作 a “inter-thread happens-before” 操作 b, 则操作 a “happens-before” 操作 b. 下图展示了这几个概念之间的关系:

Info
虽然 sequenced-before 和 inter-thread happens-before 都有传递性, 但是 happens-before 没有传递性.
假设下面的代码中 unlock() 操作 “synchronizes-with” lock() 操作.
sample
void thread1()
{
a += 1 // (1)
unlock(); // (2)
}
void thread2()
{
lock(); // (3)
cout << a << endl; // (4)
}
/* 假设直到 thread1 执行到 (2) 之前, thread2 都会阻塞在 (3) 处的 lock() 中. 那么可以推导出:
* 根据语句顺序, 有 (1) “sequenced-before” (2) 且 (3) “sequenced-before” (4);
* 因为 (2) “synchronizes-with” (3) 且 (3) “sequenced-before” (4), 所以 (2) “inter-thread happens-before” (4);
* 因为 (1) “sequenced-before” (2) 且 (2) “inter-thread happens-before” (4), 所以 (1) “inter-thread happens-before” (4); 所以 (1) “happens-before” (4).
* 因此 (4) 可以读到 (1) 对变量 a 的修改
*/
Happens-before 不代表指令实际的执行顺序
需要说明的是, happens-before 是 C++ 语义层面的概念, 它并不代表指令在 CPU 中实际的执行顺序.
为了优化性能, 编译器会在不破坏语义的前提下对指令重排. 例如:
sample
extern int a, b;
int add()
{
a++;
b++;
return a + b;
}
// 虽然有 a++; “happens-before” b++;,
// 但编译器实际生成的指令可能是先加载 a, b 两个变量到寄存器, 接着分别执行 “加一” 操作, 然后再执行 a + b, 最后才将自增的结果写入内存.
add():
movl a(%rip), %eax # 将变量 a 加载到寄存器
movl b(%rip), %ecx # 将变量 b 加载到寄存器
addl $1, %eax # a 的值加一
leal 1(%rcx), %edx # b 的值加一
movl %eax, a(%rip) # 将 a 加一的结果写入内存
addl %edx, %eax # a + b
movl %edx, b(%rip) # 将 b 加一的结果写入内存
ret
// 上面展示了 x86-64 下的一种可能的编译结果.
// 可以看到 C++ 的一条语句可能产生多条指令, 这些指令都是交错执行的.
// 其实编译器甚至还有可能先自增 b 再自增 a. 这样的重排并不会影响语义, 两个自增操作的结果仍然对 return a + b; 可见
内存顺序
前面我们提到 C++ 的六种内存顺序相互组合可以实现三种顺序模型. 现在我们来具体看看如何使用这六种内存顺序, 以及怎样的组合可以实现 synchronizes-with 的关系.
- Sequencial consistent ordering
- Acquire-relese ordering
- Relaxed ordering
内存的顺序描述了计算机CPU获取内存的顺序,内存的排序可能静态也可能动态的发生:
- 静态内存排序:编译器期间,编译器对内存重排
- 动态内存排序:运行期间,CPU乱序执行
memory_order_seq_cst
memory_order_seq_cst 可以用于 store, load 和 read-modify-write 操作, 实现 sequencial consistent 的顺序模型.
在这个模型下, 所有线程看到的所有操作都有一个一致的顺序, 即使这些操作可能针对不同的变量,
运行在不同的线程. 上面的章节中我们介绍了修改顺序 (modification order), 即单一变量的修改顺序在所有线程看来都是一致的.
Sequencial consistent 则将这种一致性扩展到了所有变量. 例如:
sample
std::atomic<bool> x{false}, y{false};
void thread1()
{
x.store(true, std::memory_order_seq_cst); // (1)
}
void thread2()
{
y.store(true, std::memory_order_seq_cst); // (2)
}
std::atomic<int> z{0};
void read_x_then_y()
{
while (!x.load(std::memory_order_seq_cst)); // (3)
if (y.load(std::memory_order_seq_cst)) ++z; // (4)
}
void read_y_then_x()
{
while (!y.load(std::memory_order_seq_cst)); // (5)
if (x.load(std::memory_order_seq_cst)) ++z; // (6)
}
int main()
{
std::thread a(thread1), b(thread2), c(read_x_then_y), d(read_y_then_x);
a.join(), b.join(), c.join(), d.join();
assert(z.load() != 0); // (7)
}
thread1和thread2分别修改原子变量x和y.
运行过程中, 有可能先执行 (1) 再执行 (2), 也有可能先执行 (2) 后执行 (1).
但无论如何, 所有线程中看到的顺序都是一致的.- (7) 处的断言永远不会失败.
因为x和y的修改顺序是全局一致的, 如果先执行 (1) 后执行 (2), 则read_y_then_x中循环 (5) 退出时, 能保证y为true, 此时x也必然为true, 因此 (6) 会被执行;
同理, 如果先执行 (2) 后执行 (1), 则循环 (3) 退出时y也必然为true, 因此 (4) 会被执行. 无论如何,z最终都不会等于 0. - Sequencial consistent 可以实现 synchronizes-with 的关系.
如果一个memory_order_seq_cst的 load 操作在某个原子变量上读到了一个memory_order_seq_cst的 store 操作在这个原子变量中写入的值, 则 store 操作 “synchronizes-with” load 操作.
在上面的例子中, 有 (1) “synchronizes-with” (3) 和 (2) “synchronizes-with” (5). - 实现 sequencial consistent 模型有一定的开销. 现代 CPU 通常有多核, 每个核心还有自己的缓存. 为了做到全局顺序一致, 每次写入操作都必须同步给其他核心.
为了减少性能开销, 如果不需要全局顺序一致, 我们应该考虑使用更加宽松的顺序模型。

- Storebuffer一级Cache, Catche是二级Cache, Memory是三级Cache
- 整体是一个4核CPU的简单结构,标识CPU的块是core, 每两个core构成一个bank, 共享一个cache. 4个core共享memory
- 每个CPU所做的store都会写道store buffer中,每个CPU会在任何时刻将store buffer的结果写入到cache或者memory中
- CPU通过MESI协议 – 作写失效(Write Invalidate)的协议来保证数据一致性。
在写失效协议里,只有一个CPU核负责写入数据,其他核心,只是同步读取到这个写入.
在这个CPU核心写入cache后,它会广播一个"失效"请求告诉所有其他的CPU核心.- M: 代表已修改, Modified
- E: 代表独占,Exclusive
- S: 代表共享, Shared
- I: 代表失效, Invalidated
- 如果变量A此刻在各个CPU的Storebuffer中,那么CPU1核修改这个a的值,放入cache时通知其他CPU核失效。
因为同一时刻只有一个CPU核可以写数据,但是其他CPU核是可以读数据的,那么其他核读取到的数据可能是CPU1核修改之前的值。 - sequencial consistent 就是为了确保所有CPU都会获取修改后的值
memory_order_relaxed
memory_order_relaxed 可以用于 store, load 和 read-modify-write 操作, 实现 Relaxed ordering 的顺序模型.
这种模型下, 只能保证操作的原子性和修改顺序 (modification order) 一致性, 无法实现 synchronizes-with 的关系.
sample
std::atomic<bool> x{false}, y{false};
void thread1()
{
x.store(true, std::memory_order_relaxed); // (1)
y.store(true, std::memory_order_relaxed); // (2)
}
void thread2()
{
while (!y.load(std::memory_order_relaxed)); // (3)
assert(x.load()); // (4)
}
thread1 对不同的变量执行 store 操作. 那么在某些线程看来, 有可能是 x 先变为 true, y 后变为 true; 另一些线程看来, 又有可能是 y 先变为 true, x 后变为 true.
(4) 处的断言就有可能失败. 因为 (2) 与 (3) 之间没有 synchronizes-with 的关系, 所以就不能保证 (1) “happens-before” (4). 因此 (4) 就有可能读到 false.
至于 relaxed 顺序模型能保证的修改顺序一致性的例子, 上面的章节中已经讨论过了, 这里就不多赘述了.
Relaxed 顺序模型的开销很小. 在 x86 架构下, memory_order_relaxed 的操作不会产生任何其他的指令, 只会影响编译器优化, 确保操作是原子的.
Relaxed 模型可以用在一些不需要线程同步的场景, 但是使用时要小心.
- 例如
std::shared_ptr增加引用计数时用的就是memory_order_relaxed, 因为不需要同步; 但是减小应用计数不能用它, 因为需要与析构操作同步.
Acquire-release
在 acquire-release 模型中, 会使用 memory_order_acquire, memory_order_release 和 memory_order_acq_rel 这三种内存顺序. 它们的用法具体是这样的:
- 对原子变量的 load 可以使用
memory_order_acquire内存顺序. 这称为 acquire 操作. - 对原子变量的 store 可以使用
memory_order_release内存顺序. 这称为 release 操作. - read-modify-write 操作即读 (load) 又写 (store), 它可以使用
memory_order_acquire,memory_order_release和memory_order_acq_rel:- 如果使用
memory_order_acquire, 则作为 acquire 操作; - 如果使用
memory_order_release, 则作为 release 操作; - 如果使用
memory_order_acq_rel, 则同时为两者.
- 如果使用
Acquire-release 可以实现 synchronizes-with 的关系. 如果一个 acquire 操作在同一个原子变量上读取到了一个 release 操作写入的值, 则这个 release 操作 “synchronizes-with” 这个 acquire 操作.
sample
std::atomic<bool> x{false}, y{false};
void thread1()
{
x.store(true, std::memory_order_relaxed); // (1)
y.store(true, std::memory_order_release); // (2)
}
void thread2()
{
while (!y.load(std::memory_order_acquire)); // (3)
assert(x.load(std::memory_order_relaxed)); // (4)
}
- 在上面的例子中, 语句 (2) 使用
memory_order_release在y中写入true, 语句 (3) 中使用memory_order_acquire从y中读取值.
循环 (3) 退出时, 它已经读取到了y的值为true, 也就是读取到了操作 (2) 中写入的值.
因此有 (2) “synchronizes-with” (3). 根据happen-before 章节介绍的规则我们可以推导出:- 因为 (2) “synchronizes-with” (3) 且 (3) “sequenced-before” (4), 所以 (2) “inter-thread happens-before” (4);
- 因为 (1) “sequenced-before” (2) 且 (2) “inter-thread happens-before” (4), 所以 (1) “inter-thread happens-before” (4);
- 所以 (1) “happens-before” (4). 因此 (4) 能读取到 (1) 中写入的值, 断言永远不会失败. 即使 (1) 和 (4) 用的是
memory_order_relaxed.
memory_order_seq_cst 章节 我们提到 sequencial consistent 模型可以实现 synchronizes-with 关系.
事实上, 内存顺序为 memory_order_seq_cst 的 load 操作和 store 操作可以分别视为 acquire 操作和 release 操作.
因此对于两个指定了 memory_order_seq_cst 的 store 操作和 load 操作, 如果后者读到了前者写入的值, 则前者 “synchronizes-with” 后者.
为了实现 synchronizes-with 关系, acquire 操作和 release 操作应该成对出现.
如果 memory_order_acquire 的 load 读到了 memory_order_relaxed 的 store 写入的值, 或者 memory_order_relaxed 的 load 读到了 memory_order_release 的 store 写入的值, 都不能实现 synchronizes-with 的关系.
虽然 sequencial consistent 模型能够像 acquire-release 一样实现同步, 但是反过来 acquire-release 模型不能像 sequencial consistent 一样提供****全局顺序一致性.
如果将memory_order_seq_cst 章节的例子中的 memory_order_seq_cst 换成 memory_order_acquire 和 memory_order_release:
sample
void thread1()
{
x.store(true, std::memory_order_release); // (1)
}
void thread2()
{
y.store(true, std::memory_order_release); // (2)
}
void read_x_then_y()
{
while (!x.load(std::memory_order_acquire)); // (3)
if (y.load(std::memory_order_acquire)) ++z; // (4)
}
void read_y_then_x()
{
while (!y.load(std::memory_order_acquire)); // (5)
if (x.load(std::memory_order_acquire)) ++z; // (6)
}
- 则最终不能保证
z不为 0. 在同一次运行中,read_x_then_y有可能看到先 (1) 后 (2), 而read_y_then_x有可能看到先 (2) 后 (1). — 这里不能保证全局顺序一致性。
这样有可能 (4) 和 (6) 的 load 的结果都为false, 导致最后z仍然为 0. - 每个线程都可以堪称一个平行宇宙,有自己的时间,而平行宇宙之间的时间是不知道。
在一个线程中,唯一能知道的是关于另外一个线程的能观察到的东西(比如 宇宙间的“happen-before”关系). - 因为任何内存顺序的Atomic Load 都不能保证加载最新值,所以所有上面的描述,Z可能最后为0.
Acquire-release 的开销比 sequencial consistent 小.
在 x86 架构下, memory_order_acquire 和 memory_order_release 的操作不会产生任何其他的指令, 只会影响编译器的优化:
任何指令都不能重排到 acquire 操作的前面, 且不能重排到 release 操作的后面; 否则会违反 acquire-release 的语义.
因此很多需要实现 synchronizes-with 关系的场景都会使用 acquire-release.
Release sequences
到目前为止我们看到的, 无论是 sequencial consistent 还是 acquire-release, 要想实现 synchronizes-with 的关系, acquire 操作必须在同一个原子变量上读到 release 操作的写入的值.
如果 acquire 操作没有读到 release 操作写入的值, 那么它俩之间通常没有 synchronizes-with 的关系. 例如:
sample
std::atomic<int> x{0}, y{0};
void thread1()
{
x.store(1, std::memory_order_relaxed); // (1)
y.store(1, std::memory_order_release); // (2)
}
void thread2()
{
y.store(2, std::memory_order_release); // (3)
}
void thread3()
{
while (!y.load(std::memory_order_acquire)); // (4)
assert(x.load(std::memory_order_relaxed) == 1); // (5)
}
- 上面的例子中, 只要
y的值非 0 循环 (4) 就会退出. 当它退出时, 有可能读到 (2) 写入的值, 也有可能读到 (3) 写入的值. - 如果是后者, 则只能保证 (3) “synchronizes-with” (4), 不能保证与 (2) 与 (4) 之间有同步关系.
- 因此 (5) 处的断言就有可能失败.
但并不是只有在 acquire 操作读取到 release 操作写入的值时才能构成 synchronizes-with 关系. 为了说这种情况, 我们需要引入 release sequence 这个概念.
针对一个原子变量 M 的 release 操作 A 完成后, 接下来 M 上可能还会有一连串的其他操作. 如果这一连串操作是由
- 同一线程上的写操作, 或者
- 任意线程上的 read-modify-write 操作
这两种构成的, 则称这一连串的操作为以 release 操作 A 为首的 release sequence. 这里的写操作和 read-modify-write 操作可以使用任意内存顺序.
如果一个 acquire 操作在同一个原子变量上读到了一个 release 操作写入的值, 或者读到了以这个 release 操作为首的 release sequence 写入的值,
那么这个 release 操作 “synchronizes-with” 这个 acquire 操作. 我们来看个例子:
sample
std::vector<int> data;
std::atomic<int> flag{0};
void thread1()
{
data.push_back(42); // (1)
flag.store(1, std::memory_order_release); // (2)
}
void thread2()
{
int expected = 1;
while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) // (3)
expected = 1;
}
void thread3()
{
while (flag.load(std::memory_order_acquire) < 2); // (4)
assert(data.at(0) == 42); // (5)
}
- 上面的例子中, (3) 处的
compare_exchange_strong是一种 read-modify-write 操作, 它判断原子变量的值是否与期望的值 (第一个参数) 相等,
如果相等则将原子变量设置成目标值 (第二个参数) 并返回true, 否则将第一个参数 (引用传递) 设置成原子变量当前值并返回false. - 操作 (3) 会一直循环检查, 当
flag当值为 1 时, 将其替换成 2. 所以 (3) 属于 (2) 的 release sequence. - 而循环 (4) 退出时, 它已经读到了 (3) 写入的值, 也就是 release 操作 (2) 为首的 release sequence 写入的值. 所以有 (2) “synchronizes-with” (4).
- 因此 (1) “happens-before” (5), (5) 处的断言不会失败.
Info
(3) 处的 compare_exchange_strong 的内存顺序是 memory_order_relaxed, 所以 (2) 与 (3) 并不构成 synchronizes-with 的关系.
也就是说, 存在当循环 (3) 退出时, 并不能保证 thread2 能读到 data.at(0) 为 42.
- 出现这种情况的原因,还是编译器或者运行时执行顺序重排导致的
- 比如在thread2循环外面加了assert(data.at(0) == 42); data和while的循环并没有依赖关系,可能会导致断言的处理优先于循环去执行。
但是 (3) 属于 (2) 的 release sequence, 当 (4) 以 memory_order_acquire 的内存顺序读到 (2) 的 release sequence 写入的值时, 可以与 (2) 构成 synchronizes-with 的关系.
memory_order_consume
Warning
C++ 17 中描述 release-consume ordering 的规范正在制定中,所以不推荐使用这个内存序,这里只是介绍相关概念
memory_order_consume 其实是 acquire-release 模型的一部分, 但是它比较特殊, 它涉及到数据间相互依赖的关系.
为此我们又要提出两个新概念: carries dependency 和 dependency-ordered before.
如果操作 a “sequenced-before” b, 且 b 依赖 a 的数据, 则 a “carries a dependency into” b. 一般来说, 如果 a 的值用作 b 的一个操作数, 或者 b 读取到了 a 写入的值, 都可以称为 b 依赖于 a. 例如
sample
p++; // (1)
i++; // (2)
p[i]; // (3)
// (1) “sequenced-before” (2) “sequenced-before” (3);
// (1) 和 (2) 的值作为 (3) 的下标运算符 [] 的操作数, 所以有 (1) “carries a dependency into” (3) 和 (2) “carries a dependency into” (3).
// 但是 (1) 和 (2) 并没有相互依赖, 它们之间没有 carries dependency 的关系.
// 从上面分析可以看到: 类似于 sequenced-before, carries dependency 关系具有传递性.
memory_order_consume 可以用于 load 操作. 使用 memory_order_consume 的 load 称为 consume 操作.
如果一个 consume 操作在同一个原子变量上读到了一个 release 操作写入的值, 或以其为首的 release sequence 写入的值, 则这个 release 操作 “dependency-ordered before” 这个 consume 操作.
Dependency-ordered before 可以 “后接” 一个 carries dependency 的关系以延伸它的范围:
如果 a “dependency-ordered before” k 且 k “carries a dependency into” b, 则 a “dependency-ordered before” b.
Dependency-ordered before 可以直接构成 inter-thread happens-before 的关系: 如果 a “dependency-ordered before” b 则 a “inter-thread happens-before” b.
概念很复杂, 但是基本思路是:
- release 操作和 acquire 操作构成的 synchronizes-with 可以后接 sequenced-before 构成 inter-thread happens-before 的关系;
- release 操作和 consume 操作构成的 dependency-ordered before 则只能后接 carries dependency 构成 inter-thread happens-before 的关系.
- 无论 inter-thread happens-before 是怎么构成的, 都可以前接 sequenced-before 以延伸其范围.

我们来看一个例子:
sample
std::atomic<std::string*> ptr;
int data;
void thread1()
{
std::string* p = new std::string("Hello"); // (1)
data = 42; // (2)
ptr.store(p, std::memory_order_release); // (3)
}
void thread2()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_consume))); // (4)
assert(*p2 == "Hello"); // (5)
assert(data == 42); // (6)
}
(4) 处的循环退出时, consume 操作 (4) 读取到 release 操作 (3) 写入的值, 因此 (3) “dependency-ordered before” (4). 由此可以推导出:
p2的值作为 (5) 的操作数, 因此 (4) “carries a dependency into” (5);- 因为 (3) “dependency-ordered before” (4) 且 (4) “carries a dependency into” (5), 所以 (3) “inter-thread happens-before” (5);
- 因为 (1) “sequenced-before” (3) 且 (3) “inter-thread happens-before” (5), 所以 (1) “inter-thread happens-before” (5);
Info
所以 (1) “happens-before” (5). 因此 (5) 可以读到 (1) 写入的值, 断言 (5) 不会失败.
但是操作 (6) 并不依赖于 (4), 所以 (3) 和 (6) 之间没有 inter-thread happens-before 的关系, 因此断言 (6) 就有可能失败.
回想上面章节强调过的 — happens-before 没有传递性. 所以不能说因为 (3) “happens-before” (4) 且 (4) “happens-before” (6) 所以 (2) “happens-before” (6).
与 consume 操作有依赖关系的指令都不会重排到 consume 操作前面.
它对重排的限制比 acquire 宽松些, acquire 要求所有的指令都不能重排到它的前面, 而 consume 只要求有依赖关系的指令不能重排到它的前面.
因此在某些情况下, consume 的性能可能会高一些.
总结
总结一下这几种内存顺序模型:
memory_order_relaxed:- 最宽松的内存顺序, 只保证操作的原子性和修改顺序 (modification order).
- 因为不考虑到线程间的同步,其他线程可能读到原子的新值,也可能读到旧值。
memory_order_acquire,memory_order_release和memory_order_acq_rel:- 实现 acquire 操作和 release 操作, 如果 acquire 操作读到了 release 操作写入的值, 或其 release sequence 写入的值, 则构成 synchronizes-with 关系, 进而可以推导出 happens-before 的关系.
- 有release内存定序的store操作进行_release operation_:当前线程中的读或写不能被重排到此存储之后(不管是编译器对代码的重排还是CPU指令重排)。
也就是说如果release对应的store操作完成了,则C++标准能够保证当前线程release之前的所有store操作肯定已经先完成了,或者说可被感知了。 - 有acquire内存定序的Load操作,在其影响的内存位置进行 acquire operation:当前线程中读或写不能被重排到此加载之前.
也就是说只有执行完此acquire对应的load操作之后,才会执行后续的读写操作。
memory_order_consume:- 实现 consume 操作, 能实现数据依赖相关的同步关系.
- 如果 consume 操作读到了 release 操作写入的值, 或其 release sequence 写入的值, 则构成 dependency-ordered before 的关系, 对于有数据依赖的操作可以进而推导出 happens-before 的关系.
- 有
consume内存定序的Load操作,在其影响的内存位置进行_consume operation_.
当前线程中依赖于当前加载的值的读或写不能被重排到此加载之前。其他线程中对有数据依赖的变量进行的释放同一原子变量的写入,能为当前线程所见。
memory_order_seq_cst:- 加强版的 acquire-release 模型, 除了可以实现 synchronizes-with 关系, 还保证在线程的全局顺序一致.
即所有线程对同一操作看到的顺序是相同的,这是默认选项。 - 由于要求全局的线程同步,因此也是开销最大的。
- 加强版的 acquire-release 模型, 除了可以实现 synchronizes-with 关系, 还保证在线程的全局顺序一致.
Warning
Basic Atomic Load : Atomic read-modify-write(RMW)
Is atomic lock-free ?
atomic 原子操作只是无锁实现的一种手段,但是原子操作本身不一定是lock-free的.
What's the latest value ?
在多线程的世界里,"最新"这个词很模糊。std::atomic只能保证在一个线程中对一个原子对象进行的读、写和读写操作是原子的,不会被打断,而且其他线程只能看到这个原子对象的前一个值或后一个值,而不会看到中间的值。
你需要把每个线程看作是一个有自己的时间的平行宇宙,而且不知道其他平行宇宙的时间。就像量子物理一样,你在一个线程中能知道另一个线程的情况的唯一方法是通过观察(比如说各个宇宙之间建立的“happen-before”的关系)。
这意味着你不应该把多线程的时间想象成有一个跨越所有线程的"最新"的值。你需要把时间想象成相对于其他线程的。
memory_order是关于在原子操作周围的非原子变量的顺序约束,这些操作被看作是fences(栅栏)。
关于这个信息以及多线程优化可以关注Herb Sutter演讲视频.
Atomic Load
从外部观察者的角度看,原子变量的最新写入值就是按照墙钟时间来看的最新值。如果有多个同时的最后写入(比如,在同一周期内在多个核心上),那么选择其中哪一个并不重要。
任何内存顺序的原子加载(atomic load)无法保证读取到最新值。这意味着写入必须在你能访问它们之前传播。这种传播可能与它们被执行的顺序不一致,也可能与不同的观察者看到的顺序不一致。
由于多线程环境中的内存访问顺序不确定性和指令重排序,为了确保正确的数据一致性和因果关系,我们需要使用更严格的内存排序(sequential consistency or acquire/release)来确保所有线程都读取到最新的全局写入值。
Atomic read-modify-write(RMW)
原子读-修改-写(RMW)操作(如交换、比较交换、fetch_add等)保证对上述定义的最新值进行操作。这意味着强制进行写入的传播,并且结果在内存中有一个统一的视图,独立于线程。
How to select ?
如果您只需要在每个线程内保持因果关系(不同的线程可能对发生的事情的顺序有不同的看法,但至少每个读者对世界的因果关系是一致的),那么atomic loads 和 (acquire/release or sequential consistency)就足够了。
但是,如果您还需要进行最新读取(也就是说,你必须永远不读取除全局(跨所有线程)最新值之外的值),那么你应该使用RMW操作进行读取。
这些操作本身并不能为非原子和非RMW读取创建因果关系,但是所有线程中的所有RMW读取都共享对世界的完全相同的视图,这个视图总是最新的.
所以,总的来说:如果允许有不同的世界观,就使用原子加载,但是如果你需要一个客观的现实,就使用RMW来加载。
Fences
https://en.cppreference.com/w/cpp/atomic/atomic_thread_fence
内存屏障(stdatomic_thread_fence(stdatomic_thread_fence(std::memory_acquire)确保所有屏障之前的读取操作都会在屏障之后的读取操作之前执行。
sample
bool x = false;
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y() {
x = true;
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed);
}
void read_y_then_x() {
while (!y.load(std::memory_order_relaxed))
;
std::atomic_thread_fence(std::memory_order_acquire);
if (x) ++z;
}
int main() {
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0);
}
这里不对fence做过多介绍,需要的时候可查询相关的使用方式。