一、std::atomic 到底解决了什么问题?

多线程编程里,最让人头疼的就是数据竞争。你想想看,两个线程同时读写同一个变量,结果会怎样?

我早年做游戏服务器时,就踩过这个坑。一个简单的计数器,两个线程各加 100 万次,结果跑出来不是 200 万。当时我盯着屏幕愣了半天——嗯,这就是典型的「非原子操作」问题。

std::atomic 的出现,就是为了解决这个。它保证了对变量的读写是「不可分割」的。说白了,要么读完了再写,要么写完了再读,不会出现读到一半的值。

核心要点: 原子操作 = 不可中断的操作。一个线程在做原子操作时,其他线程看不到中间状态。

二、原子类型一览

C++11 标准库提供了一组原子类型,都在 <atomic> 头文件里。常用的有这些:

原子类型 对应的普通类型 说明
std::atomic<bool> bool 原子布尔值
std::atomic<int> int 原子整型,最常用
std::atomic<unsigned int> unsigned int 无符号版本
std::atomic<long long> long long 64位原子整型
std::atomic<T*> T* 原子指针
std::atomic_flag 最简单的原子标志,保证无锁

我个人习惯用 std::atomic<int>std::atomic<bool> 解决 90% 的场景。剩下的 10%,才需要上 CAS 或者自定义原子类型。

小提示: std::atomic_flag 是唯一一个保证在所有平台上都是无锁的原子类型。如果你只需要一个简单的标志位,用它准没错。

三、CAS 操作:原子世界的「比较-交换」

CAS 全称是 Compare-And-Swap,也叫 Compare-And-Exchange。它的逻辑很简单:

bool atomic_compare_exchange_weak(T* expected, T desired);
bool atomic_compare_exchange_strong(T* expected, T desired);

什么意思呢?就是「如果当前值等于 expected,就把它换成 desired,返回 true;否则把当前值写回 expected,返回 false」。

我在项目中遇到过这样一个场景:多个线程要抢一个任务队列里的任务。用 CAS 实现一个无锁的 pop 操作,比用 mutex 快了好几倍。

std::atomic<int> counter{0};

void thread_func() {
    int expected = 0;
    int desired = 1;
    // 尝试把 counter 从 0 改成 1
    if (counter.compare_exchange_strong(expected, desired)) {
        // 成功了,说明我是第一个
    } else {
        // 失败了,expected 被更新为当前值
    }
}
注意: compare_exchange_weak 和 compare_exchange_strong 的区别在于,weak 版本在某些平台上可能出现「伪失败」(spurious failure),需要配合循环使用。strong 版本则保证要么成功要么失败,没有中间状态。

四、内存屏障:看不见的秩序

原子操作只是保证了「原子性」,但没保证「顺序性」。你想想看,CPU 和编译器为了优化,可能会重排指令。这就引出了内存屏障的概念。

std::atomic 的每个操作都可以指定一个内存序(memory_order),常见的有:

  • memory_order_relaxed:最宽松,只保证原子性,不保证顺序
  • memory_order_acquire:之后的读写不能重排到该操作之前
  • memory_order_release:之前的读写不能重排到该操作之后
  • memory_order_acq_rel:acquire + release 的结合
  • memory_order_seq_cst:最严格,全局顺序一致(默认值)

我曾经在优化一个高频交易系统时,把默认的 seq_cst 改成了 acquire-release,性能提升了 30%。但代价是——你得非常清楚自己在做什么。

经验之谈: 如果你不确定用哪个内存序,就用默认的 memory_order_seq_cst。它虽然慢一点,但不会出错。等性能瓶颈出现了,再考虑优化。

五、无锁数据结构基础

无锁数据结构,说白了就是不用 mutex,全靠原子操作和 CAS 来保证线程安全。它的核心思想是:如果操作失败,就重试,而不是阻塞。

一个最简单的无锁栈:

template<typename T>
class LockFreeStack {
    struct Node {
        T data;
        Node* next;
    };
    std::atomic<Node*> head{nullptr};

public:
    void push(T value) {
        Node* new_node = new Node{value, nullptr};
        new_node->next = head.load(std::memory_order_relaxed);
        while (!head.compare_exchange_weak(
            new_node->next, new_node,
            std::memory_order_release,
            std::memory_order_relaxed)) {
            // 重试,直到成功
        }
    }

    bool pop(T& value) {
        Node* old_head = head.load(std::memory_order_relaxed);
        while (old_head && !head.compare_exchange_weak(
            old_head, old_head->next,
            std::memory_order_acquire,
            std::memory_order_relaxed)) {
            // 重试
        }
        if (old_head) {
            value = old_head->data;
            delete old_head;
            return true;
        }
        return false;
    }
};

这段代码看起来简单,但有个坑——ABA 问题。什么意思呢?就是线程 A 读到了 head 指向节点 X,然后线程 B 把 X 弹出去又 push 回来,head 还是指向 X,但 X 的内容已经变了。线程 A 的 CAS 会成功,但拿到的数据是错的。

避坑指南: 我曾经在实现无锁队列时忽略了 ABA 问题,结果线上服务跑了两天突然崩溃。排查了整整一个下午才发现是 ABA 导致的。解决方案是用带标记的指针(tagged pointer)或者 hazard pointer。

六、知识体系总览

下面这张图,是我梳理的本章知识结构。你可以把它当作一张地图,随时回来查阅。

std::atomic 详解 原子类型 std::atomic<int> std::atomic<bool> std::atomic<T*> std::atomic_flag CAS 操作 compare_exchange_weak compare_exchange_strong ABA 问题 内存屏障 memory_order_relaxed memory_order_acquire memory_order_release memory_order_seq_cst 无锁数据结构基础 无锁栈 无锁队列 Hazard Pointer RCU

七、总结与建议

std::atomic 是 C++ 并发编程的基石。我个人觉得,掌握它需要分三步走:

  1. 先会用:知道怎么声明原子变量,怎么读写
  2. 再理解:搞懂 CAS 和内存序的含义
  3. 最后优化:在性能敏感的场景下,选择合适的 memory_order

记住一点:无锁编程不是银弹。它比 mutex 更难调试,更容易出错。我见过太多人一上来就搞无锁队列,结果 bug 修了一个月。如果你只是需要保护一个共享变量,用 std::atomic 就够了。如果你需要保护一段代码,老老实实用 std::mutex。

我的建议: 先从 std::atomic<int> 和默认的 memory_order_seq_cst 开始。等你对并发编程有了感觉,再慢慢深入 CAS 和内存屏障。别一口吃成胖子。

公众号:蓝海资料掘金营,微信 deep3321