跳至内容

原子操作与内存序

理解使用 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()。你也可以使用运算符 ++、--、+= 等。

atomic_counter.cpp
#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 ── 除了原子性之外没有任何序保证。操作是原子的,但可以被自由重排。仅用于不关心顺序的独立计数器或统计数据。

获取-释放实践

获取-释放模式是无锁编程的核心工具。一个线程中的释放存储与另一个线程中的获取加载同步,建立起先行发生关系。

acquire_release.cpp
#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)是无锁数据结构的基本构建块。它原子地检查值是否等于期望值,如果是,则将其替换为目标值。

cas_max.cpp
#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(),非常适合实现简单的自旋锁。

spinlock.cpp
#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::is_lock_free() 验证无锁假设。

关键要点
  • 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` 是安全的?