原子操作与内存序
理解使用 std::atomic 的无锁编程。掌握内存序(relaxed、acquire、release、seq_cst),使用 atomic_flag 实现自旋锁,并学习比较并交换(CAS)模式。
什么是原子操作?
原子操作是不可分割的 ── 它要么完全完成,要么完全不执行,其他线程看不到中间状态。std::atomic 提供对类型为 T 的值的原子访问,无需互斥锁。
考虑一个在多个线程间共享的普通 int 上执行 ++counter。这实际上是三个步骤:加载、递增、存储。另一个线程可以在这些步骤之间交叉执行,导致数据竞争(未定义行为)。使用 std::atomic 时,递增是一个单一的原子操作 ── 没有交叉,没有未定义行为。
但原子操作不仅仅是「不可分割的操作」。它们还控制内存序 ── 规定一个线程的写入何时对其他线程可见的规则。这是原子操作既强大又微妙之处。
std::atomic 基础
最常见的原子操作是 load()、store()、exchange() 和 fetch_add()/fetch_sub()。你也可以使用运算符 ++、--、+= 等。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter{0}; // atomic integer, initialized to 0
void increment(int n) {
for (int i = 0; i < n; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
// or simply: ++counter; (uses seq_cst by default)
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment, 100'000);
}
for (auto& t : threads) t.join();
std::cout << "Counter: " << counter.load() << "\n";
// Always prints 1000000 — no data race, no mutex needed
return 0;
}内存序详解
现代 CPU 和编译器会为了提高性能而重排内存操作。在 x86 上这相对温和,但在 ARM/RISC-V 上则非常激进。内存序告诉编译器和 CPU 哪些重排是允许的。
memory_order_seq_cst(默认) ── 最强的内存序。所有线程以相同的总顺序看到所有 seq_cst 操作。简单且安全,但可能限制优化。
memory_order_acquire ── 用于加载操作。当前线程中的任何内存读/写都不能被重排到此加载之前。可以理解为:「我在此之后读到的一切,至少能看到释放线程所写的内容。」
memory_order_release ── 用于存储操作。当前线程中的任何内存读/写都不能被重排到此存储之后。可以理解为:「我在此之前写的一切,对获取线程都是可见的。」
memory_order_acq_rel ── 结合获取和释放。用于 compare_exchange 等读-修改-写操作。
memory_order_relaxed ── 除了原子性之外没有任何序保证。操作是原子的,但可以被自由重排。仅用于不关心顺序的独立计数器或统计数据。
获取-释放实践
获取-释放模式是无锁编程的核心工具。一个线程中的释放存储与另一个线程中的获取加载同步,建立起先行发生关系。
#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> data_ready{false};
int payload = 0; // non-atomic — protected by acquire-release
void producer() {
payload = 42; // write payload BEFORE the release store
data_ready.store(true, std::memory_order_release);
// release: all writes before this store are visible
// to a thread that does an acquire load of data_ready
}
void consumer() {
while (!data_ready.load(std::memory_order_acquire)) {
// spin — acquire: sees all writes before the release store
}
// guaranteed to see payload == 42
assert(payload == 42); // never fires
std::cout << "Payload: " << payload << "\n";
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}比较并交换(CAS)模式
比较并交换(compare_exchange_weak/compare_exchange_strong)是无锁数据结构的基本构建块。它原子地检查值是否等于期望值,如果是,则将其替换为目标值。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> max_value{0};
void update_max(int new_val) {
int current = max_value.load(std::memory_order_relaxed);
// CAS loop: keep trying until we succeed or new_val is not bigger
while (new_val > current) {
if (max_value.compare_exchange_weak(
current, // expected (updated on failure)
new_val, // desired
std::memory_order_relaxed)) {
break; // success
}
// On failure, 'current' is updated with the actual value
// Loop re-checks if new_val > current (maybe another thread won)
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 100; ++i) {
threads.emplace_back(update_max, i);
}
for (auto& t : threads) t.join();
std::cout << "Max: " << max_value.load() << "\n"; // always 99
return 0;
}使用 std::atomic_flag 实现自旋锁
std::atomic_flag 是最原始的原子类型 ── 在每个平台上都保证是无锁的。它只支持 test_and_set() 和 clear(),非常适合实现简单的自旋锁。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
class SpinLock {
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {
// spin — consider adding a pause/yield hint:
// __builtin_ia32_pause() or std::this_thread::yield()
}
}
void unlock() {
flag_.clear(std::memory_order_release);
}
};
SpinLock spin;
int shared_data = 0;
void work(int n) {
for (int i = 0; i < n; ++i) {
spin.lock();
++shared_data; // protected by spinlock
spin.unlock();
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(work, 100'000);
}
for (auto& t : threads) t.join();
std::cout << "shared_data: " << shared_data << "\n";
// prints 400000
return 0;
}memory_order_relaxed 除了原子性本身之外不提供任何序保证。用它来保护共享的非原子数据(如用标志守护载荷数据)是一个错误:
// 错误:relaxed 存储/加载不会同步 payload
payload = 42;
ready.store(true, memory_order_relaxed); // 可能被重排到 payload=42 之前!
// 消费者:
while (!ready.load(memory_order_relaxed)) {}
assert(payload == 42); // 在 ARM/POWER 上可能失败仅当原子变量独立存在且不需要同步对其他数据的访问时,才使用 memory_order_relaxed。对于发布-消费模式,请使用 release/acquire。
从 memory_order_seq_cst(默认值)开始,只有在性能分析证明有必要时才放宽到更弱的内存序。错误的内存序会产生在 x86 上几乎无法重现但在 ARM 上会显现的错误。对于复杂的共享状态,优先使用互斥锁 ── 原子操作适用于简单的共享变量和构建无锁原语。始终使用 std::atomic 验证无锁假设。
std::atomic提供无需互斥锁的不可分割操作 ── 对并发读写是安全的- 内存序控制一个线程的写入何时对其他线程可见 ── 它不仅仅关于原子性
- 获取-释放是关键模式:释放存储发布数据,获取加载消费数据
compare_exchange_weak/strong(CAS)是无锁算法的构建块std::atomic_flag是所有平台上唯一保证无锁的类型- 默认使用
seq_cst,仅在性能分析要求时才放宽 ── 微妙的错误极难调试
测验 — 检验你的知识
(25 经验值)1. `memory_order_acquire` 在加载操作上保证了什么?
2. 为什么 `compare_exchange_weak` 通常在循环中使用?
3. 什么时候使用 `memory_order_relaxed` 是安全的?