📋 前置准备
所有代码均使用 C++17,在 Linux 下用 g++ 编译。请确保安装了编译工具链:
bash
sudo apt install g++
通用编译命令模板:
bash
# 普通编译(用于看正常逻辑)
g++ -std=c++17 -pthread -o demo demo.cpp
# 带 ThreadSanitizer 编译(用于抓数据竞争)
g++ -std=c++17 -pthread -fsanitize=thread -g -o demo_tsan demo.cpp
示例 1:std::memory_order_relaxed (纯原子计数器)
场景:只需要保证"自增"操作本身是原子的,不需要线程间同步其他数据。
1.1 完整代码 (demo1_relaxed.cpp)
cpp
#include <atomic>
#include <thread>
#include <iostream>
#include <cassert>
// 全局原子计数器
std::atomic<int> g_counter(0);
// 线程函数:循环自增 100 万次
void increment_1m_times() {
for (int i = 0; i < 1000000; ++i) {
// 关键点:使用 relaxed
// 只保证 fetch_add 是原子的,不保证任何内存顺序
g_counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::cout << "Start (Relaxed)..." << std::endl;
// 启动两个线程同时自增
std::thread t1(increment_1m_times);
std::thread t2(increment_1m_times);
t1.join();
t2.join();
// 验证结果
int result = g_counter.load(std::memory_order_relaxed);
std::cout << "Final counter: " << result << std::endl;
// Relaxed 保证了原子性,结果一定是 2000000
assert(result == 2000000);
return 0;
}
1.2 编译与运行
bash
g++ -std=c++17 -pthread -o demo1 demo1_relaxed.cpp
./demo1
输出:
text
Start (Relaxed)...
Final counter: 2000000
1.3 一步一步详解
- 原子性保证 :
fetch_add(1, relaxed)确保了"读取-修改-写入"这三步是不可分割的。如果这里用的是普通int g_counter和g_counter++,结果会小于 200万。 - 无顺序保证:虽然结果正确,但线程 A 并不知道线程 B 已经自增到哪一步了。它们只是各算各的,最后汇总。
- 性能最高:这是最快的内存序,因为 CPU 和编译器可以自由重排指令。
示例 2:std::memory_order_release & std::memory_order_acquire (生产者-消费者)
场景 :线程 A 准备数据,线程 B 读取数据。我们需要保证:B 看到"就绪信号"时,一定能看到 A 准备好的数据。
2.1 完整代码 (demo2_release_acquire.cpp)
cpp
#include <atomic>
#include <thread>
#include <iostream>
#include <cassert>
// 普通共享数据(非原子)
int g_data = 0;
// 原子信号量
std::atomic<bool> g_ready(false);
void producer() {
// 步骤 1: 先写数据(这是普通的非原子操作)
g_data = 42;
std::cout << "Producer: Data prepared." << std::endl;
// 步骤 2: 发布信号 (使用 Release)
// 🔑 关键规则:g_data = 42 绝对不会被重排到这行 store 之后
g_ready.store(true, std::memory_order_release);
}
void consumer() {
// 步骤 1: 自旋等待信号 (使用 Acquire)
while (!g_ready.load(std::memory_order_acquire)) {
// 空转等待
}
std::cout << "Consumer: Signal received." << std::endl;
// 步骤 2: 读取数据
// 🔑 关键规则:因为上面是 Acquire,这里一定能看到 g_data = 42
assert(g_data == 42);
std::cout << "Consumer: Data is " << g_data << " (Success!)" << std::endl;
}
int main() {
std::thread t_prod(producer);
std::thread t_cons(consumer);
t_prod.join();
t_cons.join();
return 0;
}
2.2 错误示范 (如果误用 Relaxed)
我们把上面代码中的 release 和 acquire 都改成 relaxed,保存为 demo2_wrong.cpp。
在 x86 CPU 上直接运行,你可能依然会看到 Success (因为 x86 硬件是强内存模型 TSO,自动帮你做了很多事)。但这是错的! 我们用 ThreadSanitizer 来抓它:
bash
g++ -std=c++17 -pthread -fsanitize=thread -g -o demo2_wrong_tsan demo2_wrong.cpp
./demo2_wrong_tsan
TSAN 输出 (关键部分):
text
WARNING: ThreadSanitizer: data race (pid=12345)
Read of size 4 at 0x55c... by thread T2:
#0 consumer() demo2_wrong.cpp:32
#1 ...
Previous write of size 4 at 0x55c... by thread T1:
#0 producer() demo2_wrong.cpp:12
#1 ...
这就证明了:用 Relaxed 会导致数据竞争 ,g_data 的读写没有被正确同步。
2.3 一步一步详解
- Release (写者):像是"关门"动作。我在关门(store)之前做的所有事(写 data),都必须在关门之前完成。
- Acquire (读者):像是"开门"动作。我开了门(load 到 true)之后,门里的东西(data)我就都能看见了。
- 配对使用:这是无锁编程中最常用的"黄金组合",性能仅次于 Relaxed,但安全性极高。
示例 3:std::memory_order_acq_rel (获取释放,用于 RMW)
场景 :当一个操作既是"读者"又是"写者"时(即 Read-Modify-Write, RMW 操作),比如 fetch_add、exchange、compare_exchange。
例子:我们用原子操作实现一个简单的"接力棒"传递。
3.1 完整代码 (demo3_acq_rel.cpp)
cpp
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> g_baton(0); // 0: 没人拿, 1: 线程1拿, 2: 线程2拿
int g_shared_value = 0;
void thread1_work() {
g_shared_value = 100; // 先干活
// 🔑 RMW 操作:把 0 换成 1
// 使用 acq_rel:
// 1. "Acquire" 部分:确保我们看到了之前的状态 (0)
// 2. "Release" 部分:确保 g_shared_value = 100 对下一个拿到的人可见
int expected = 0;
while (!g_baton.compare_exchange_weak(expected, 1, std::memory_order_acq_rel)) {
expected = 0; // 失败了重置 expected
}
std::cout << "Thread 1: Passed the baton." << std::endl;
}
void thread2_work() {
// 等待拿到 baton (1),然后设为 2
int expected = 1;
while (!g_baton.compare_exchange_weak(expected, 2, std::memory_order_acq_rel)) {
expected = 1;
}
// 因为 Thread 1 用了 Release,这里用了 Acquire,所以能看到 100
std::cout << "Thread 2: Got baton. Shared value = " << g_shared_value << std::endl;
}
int main() {
std::thread t1(thread1_work);
std::thread t2(thread2_work);
t1.join();
t2.join();
return 0;
}
3.2 一步一步详解
- RMW 的特殊性 :
compare_exchange先读(看是不是 expected),再写(改成 desired)。 - Acq_Rel 的作用 :
- 读的那一瞬间 :它是
Acquire,确保能看到之前线程的写入。 - 写的那一瞬间 :它是
Release,确保自己的写入对后续线程可见。
- 读的那一瞬间 :它是
- 适用场景:实现自旋锁(Spinlock)、无锁队列的节点插入等。
示例 4:std::memory_order_seq_cst (顺序一致性,默认选项)
场景 :需要全局总序 (Total Order)。即:所有线程看到的所有原子操作的发生顺序是完全一致的。
这是 C++ 原子操作的默认内存序(如果你不写参数,就是这个)。
4.1 完整代码 (demo4_seq_cst.cpp)
这是一个经典的 IRIW (Independent Read Independent Write) 示例。
cpp
#include <atomic>
#include <thread>
#include <iostream>
#include <cassert>
std::atomic<bool> x{false}, y{false};
std::atomic<int> z{0};
void write_x() {
x.store(true, std::memory_order_seq_cst);
}
void write_y() {
y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y() {
while (!x.load(std::memory_order_seq_cst)); // 等 x
if (y.load(std::memory_order_seq_cst)) { // 看 y
z.fetch_add(1, std::memory_order_relaxed);
}
}
void read_y_then_x() {
while (!y.load(std::memory_order_seq_cst)); // 等 y
if (x.load(std::memory_order_seq_cst)) { // 看 x
z.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
// 循环跑 1000 次,增加概率观察结果
for (int i = 0; i < 1000; ++i) {
x = false; y = false; z = 0;
std::thread a(write_x), b(write_y);
std::thread c(read_x_then_y), d(read_y_then_x);
a.join(); b.join(); c.join(); d.join();
// 🔑 Seq_Cst 保证:z 绝对不可能是 0
// 因为所有操作有一个全局顺序,要么 x 在 y 前,要么 y 在 x 前
// 至少有一个读线程会看到两个都是 true
assert(z.load() != 0);
}
std::cout << "All tests passed (Seq_Cst guarantees no z=0)." << std::endl;
return 0;
}
4.2 一步一步详解
- 全局时钟 :
seq_cst就像给整个程序安了一个全局时钟。所有原子操作按时间戳排列,所有线程看到的顺序都一样。 - 为什么 z 不能为 0 :
- 如果是
acq_rel,理论上可能出现:线程 C 看到x=true但y=false,同时线程 D 看到y=true但x=false(因为没有全局总序),导致z=0。 - 但
seq_cst禁止了这种情况。
- 如果是
- 性能代价 :这是最慢的内存序(在 x86 上通常需要
MFENCE指令)。但它是最直观、最不容易出错的。
📊 最终总结与决策树
为了让你好记,我做了一个简单的决策流程:
- 我只是做个计数器/统计,不依赖它同步别的数据?
- ✅ 用
std::memory_order_relaxed
- ✅ 用
- 我有两个线程,一个发信号,一个收信号,收信号后要读发信号前写的数据?
- ✅ 发信号用
std::memory_order_release - ✅ 收信号用
std::memory_order_acquire
- ✅ 发信号用
- 我在做
fetch_add/compare_exchange这种 RMW 操作,需要它承上启下?- ✅ 用
std::memory_order_acq_rel
- ✅ 用
- 我不知道该用什么 / 逻辑很复杂 / 图省事 / 需要全局顺序?
- ✅ 用
std::memory_order_seq_cst(默认)
- ✅ 用