3. C++原子操作详解:atomic类型、load/store操作、fetch_add/compare_exchange等原子操作、memory_order参数详解

说到多线程编程,锁是个好东西,但有时候太重了。我早年做游戏服务器时,一个高频计数器用互斥锁保护,结果性能直接掉了一半。后来换成原子操作,问题迎刃而解。说白了,原子操作就是让你在无锁的情况下,安全地读写共享变量。

3.1 什么是原子操作?

原子操作,就是不可分割的操作。一个线程在做原子操作时,其他线程看不到中间状态。要么全部完成,要么什么都没做。这跟数据库事务的原子性是一个道理。

我遇到过不少新手,以为 i++ 是原子的。其实不是。它至少包含三步:读 i、加 1、写回。两个线程同时执行,结果可能不是你想要的。这时候就需要 std::atomic 出马了。

核心要点:原子操作是构建无锁数据结构的基础,也是实现高性能并发编程的关键工具。

3.2 atomic 类型

C++11 引入了 std::atomic 模板类。你可以用它包装任何 trivially copyable 的类型。不过,最常用的还是整数类型和指针。

#include <atomic>

std::atomic<int> counter(0);
std::atomic<bool> flag(false);
std::atomic<uint64_t> timestamp(0);
std::atomic<int*> ptr(nullptr);

对于整数类型,还有特化版本,比如 std::atomic_int、std::atomic_uint64_t。我个人习惯直接用 std::atomic<int>,更统一,也更容易读。

小提示:atomic 对象默认是锁实现的,但对于 int、long 等基础类型,编译器通常会生成无锁的机器指令。可以用 is_lock_free() 检查。

3.3 load 和 store 操作

最基本的两个操作:读和写。但原子操作的读写,比普通读写多了个 memory_order 参数。

std::atomic<int> value(42);

// 读操作
int x = value.load(std::memory_order_acquire);

// 写操作
value.store(100, std::memory_order_release);

不传 memory_order 的话,默认是 memory_order_seq_cst,最严格但也最慢。我一般只在需要强一致性的场景用默认值,其他时候会选更宽松的顺序。

注意:普通变量的读写不是原子的。即使只是读一个 int,在多线程环境下也可能读到旧值或乱序的值。用 atomic<int> 的 load 才能保证。

3.4 fetch_add / fetch_sub

这两个操作对应的是「读-改-写」模式。比如计数器加一,用 fetch_add 一步搞定。

std::atomic<int> counter(0);

// 原子加 1,返回旧值
int old = counter.fetch_add(1);

// 原子减 1
counter.fetch_sub(1);

// 也可以用 += 运算符,等价于 fetch_add
counter += 1;

我记得有一次,线上服务有个统计计数器,用互斥锁保护。压测时发现锁竞争严重。改成 fetch_add 后,性能提升了 3 倍。嗯,这就是原子操作的价值。

3.5 compare_exchange

这是原子操作的「瑞士军刀」。它实现了一个原子的「比较并交换」逻辑:如果当前值等于期望值,就更新为目标值;否则,把当前值写回期望值。

std::atomic<int> value(0);

int expected = 0;
int desired = 42;

// 如果 value == expected,则 value = desired,返回 true
// 否则,expected = value,返回 false
bool success = value.compare_exchange_weak(expected, desired);

if (success) {
    // 更新成功
} else {
    // 更新失败,expected 已被更新为当前值
}

这里有两个版本:compare_exchange_weak 和 compare_exchange_strong。weak 版本在某些平台上可能「虚假失败」,需要循环重试。strong 版本保证要么成功要么失败。我一般用 strong,除非在性能敏感的循环里才用 weak。

实战场景:实现无锁链表、无锁栈、无锁队列时,compare_exchange 是核心原语。我曾在项目中用 CAS 实现了一个无锁的引用计数,避免了智能指针的锁开销。

3.6 memory_order 参数详解

这是原子操作最让人头疼的部分。说白了,memory_order 控制的是「内存可见性」和「指令重排」的边界。C++ 提供了 6 种顺序,从松到严:

memory_order 含义 性能
relaxed 只保证原子性,不保证顺序 最快
consume 数据依赖顺序(很少用) 较快
acquire 之后的读写不能重排到之前 中等
release 之前的读写不能重排到之后 中等
acq_rel acquire + release 较慢
seq_cst 全局顺序一致 最慢

我画了一张图,帮你理解这些顺序的关系:

memory_order 强度与性能关系 强 弱 性能 低 高 relaxed consume acquire release acq_rel seq_cst

你想想看,为什么需要这么多顺序?因为不同的场景对一致性的要求不一样。比如一个统计计数器,只要求最终结果正确,用 relaxed 就够了。但如果你要实现一个无锁队列,生产者消费者之间需要同步,那就得用 release/acquire 配对。

3.7 实际应用:无锁计数器

来看一个完整的例子。假设我们要统计某个接口的调用次数:

#include <atomic>
#include <thread>
#include <iostream>

class AtomicCounter {
private:
    std::atomic<uint64_t> count_{0};
    
public:
    // 计数加一,用 relaxed 就够了
    void increment() {
        count_.fetch_add(1, std::memory_order_relaxed);
    }
    
    // 读取当前值,需要 acquire 保证看到最新值
    uint64_t get() const {
        return count_.load(std::memory_order_acquire);
    }
};

// 使用示例
void worker(AtomicCounter& counter, int times) {
    for (int i = 0; i < times; ++i) {
        counter.increment();
    }
}

int main() {
    AtomicCounter counter;
    
    std::thread t1(worker, std::ref(counter), 1000000);
    std::thread t2(worker, std::ref(counter), 1000000);
    
    t1.join();
    t2.join();
    
    std::cout << "Final count: " << counter.get() << std::endl;
    // 输出:2000000
    return 0;
}

这里有个细节:increment 用 relaxed,因为多个线程的加操作之间没有顺序要求。但 get 用 acquire,因为读取线程需要看到所有写入线程的最终结果。这就是 release-acquire 语义的典型用法。

避坑指南:我曾经在项目里把所有原子操作都用了 seq_cst,结果性能比用锁还差。后来分析发现,大部分场景用 relaxed 或 acquire/release 就足够了。记住:最严格的顺序不一定是最好的。

3.8 总结

原子操作是并发编程的基石。掌握 atomic 类型、load/store、fetch_add、compare_exchange 以及 memory_order,你就能写出高效且正确的无锁代码。嗯,刚开始可能会觉得 memory_order 很绕,但多写几个例子就明白了。关键是要想清楚:你的线程之间需要什么样的可见性保证?

可可精品资料库