浏览知识库目录

C++

std::atomic

std::atomic

std::atomic 是 C++ 标准库提供的原子类型,用于多线程环境下安全地读写共享数据。

它的核心保证是:对原子对象的一次操作不会被其他线程“做到一半时插入”。但要注意:

原子变量只能保证针对该原子对象的操作安全,并不会自动让一整段代码都线程安全。

需要包含:

#include <atomic>

1. 基本用法

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

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

void work()
{
    for (int i = 0; i < 100000; ++i) {
        ++counter;
    }
}

int main()
{
    std::vector<std::thread> threads;

    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(work);
    }

    for (auto& t : threads) {
        t.join();
    }

    std::cout << counter.load() << '\n'; // 400000
}

如果使用普通 int

int counter = 0;

多个线程执行 ++counter 会产生数据竞争,程序行为未定义。因为 ++counter 实际包含读取、加一和写回三个步骤。

2. 初始化

推荐直接初始化:

std::atomic<int> count{0};
std::atomic<bool> ready{false};
std::atomic<int*> ptr{nullptr};

原子对象不能像普通变量一样复制:

std::atomic<int> a{10};

// std::atomic<int> b = a; // 错误
// b = a;                  // 错误

应该显式读取和写入:

std::atomic<int> a{10};
std::atomic<int> b{a.load()};

b.store(a.load());

3. load()store()

load():原子读取

std::atomic<int> value{10};

int x = value.load();

store():原子写入

value.store(20);

很多时候也能使用类似普通变量的语法:

value = 30;
int x = value;

但显式写成 load()store() 通常更清楚,也方便指定内存序。

4. exchange():交换并返回旧值

std::atomic<int> value{10};

int old = value.exchange(20);

std::cout << old << '\n';          // 10
std::cout << value.load() << '\n'; // 20

它相当于原子地执行:

old = value;
value = 20;

常见用途是实现简单的状态切换:

std::atomic<bool> busy{false};

if (!busy.exchange(true)) {
    // 当前线程成功将 false 改为 true
    // 执行独占任务

    busy.store(false);
}

不过真正需要保护临界区时,通常优先使用 std::mutex,不要随意手写锁。

5. 原子加减操作

对于整数原子类型,可以使用:

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

count.fetch_add(3); // 加 3
count.fetch_sub(2); // 减 2

++count;
count++;
--count;
count--;

fetch_add() 返回修改之前的值:

std::atomic<int> count{10};

int old = count.fetch_add(5);

std::cout << old << '\n';          // 10
std::cout << count.load() << '\n'; // 15

前置和后置运算符的返回规则与普通整数类似:

std::atomic<int> count{10};

int a = ++count; // a == 11
int b = count++; // b == 11,之后 count == 12

整数原子还支持:

value.fetch_and(mask);
value.fetch_or(mask);
value.fetch_xor(mask);

例如设置状态位:

constexpr unsigned readable = 1u << 0;
constexpr unsigned writable = 1u << 1;

std::atomic<unsigned> flags{0};

flags.fetch_or(readable);       // 设置 readable
flags.fetch_or(writable);       // 设置 writable
flags.fetch_and(~readable);     // 清除 readable

6. compare_exchange:比较并交换

这是无锁算法中最重要的操作之一。

其逻辑类似:

if (atomic_value == expected) {
    atomic_value = desired;
    return true;
} else {
    expected = atomic_value;
    return false;
}

实际代码:

std::atomic<int> value{10};

int expected = 10;

bool success = value.compare_exchange_strong(expected, 20);

成功时:

success == true
value == 20
expected == 10

失败时,expected 会被更新为原子变量当前的值:

std::atomic<int> value{15};

int expected = 10;
bool success = value.compare_exchange_strong(expected, 20);

// success == false
// value == 15
// expected == 15

循环更新示例

假设要原子地更新最大值:

void update_max(std::atomic<int>& maximum, int candidate)
{
    int current = maximum.load();

    while (current < candidate &&
           !maximum.compare_exchange_weak(current, candidate)) {
        // 失败后,current 已自动更新为 maximum 的最新值
    }
}

也可以写成:

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

void update_max(int candidate)
{
    int current = maximum.load();

    while (candidate > current) {
        if (maximum.compare_exchange_weak(current, candidate)) {
            break;
        }
    }
}

C++26 为部分原子类型增加了 fetch_max()fetch_min();如果项目使用更早标准,则需要上述 CAS 循环。

7. weakstrong 的区别

提供两个比较交换函数:

compare_exchange_weak()
compare_exchange_strong()

compare_exchange_weak

允许“伪失败”:即使当前值等于 expected,也可能返回 false

适合放在循环中:

while (!value.compare_exchange_weak(expected, desired)) {
    // 重试
}

compare_exchange_strong

不会因为伪失败而失败,适合只尝试一次的场景:

if (value.compare_exchange_strong(expected, desired)) {
    // 修改成功
}

简单记忆:

  • 循环重试:通常用 weak
  • 只判断一次:通常用 strong

8. 常见原子类型

基本形式

std::atomic<bool>
std::atomic<int>
std::atomic<unsigned>
std::atomic<long>
std::atomic<MyType>

标准库还提供了一些别名:

std::atomic_bool
std::atomic_int
std::atomic_uint
std::atomic_long
std::atomic_size_t

例如:

std::atomic_int count{0};
std::atomic_bool stopped{false};

自定义类型

某些简单类型可以作为 std::atomic<T> 的参数:

struct Point {
    int x;
    int y;
};

static_assert(std::is_trivially_copyable_v<Point>);

std::atomic<Point> point{{1, 2}};

一般要求 T 是可平凡复制类型。即使允许构造,也不代表它一定使用硬件无锁指令。

9. is_lock_free()

可以检查当前原子类型在当前平台上是否无锁:

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

std::cout << std::boolalpha
          << value.is_lock_free()
          << '\n';

编译期属性:

std::atomic<int>::is_always_lock_free

例如:

static_assert(std::atomic<int>::is_always_lock_free);

不过这条断言并不一定在所有平台都成立。

重要区别:

  • 原子操作:语义上不可分割、线程安全
  • 无锁实现:底层实现没有使用锁

std::atomic<T> 保证原子性,但不保证一定是无锁实现。

10. 内存序 memory_order

原子操作不仅涉及“操作是否被打断”,还涉及:

  • 编译器能否调整指令顺序
  • CPU 能否调整内存访问顺序
  • 一个线程何时能看到另一个线程的普通数据修改

默认内存序是:

std::memory_order_seq_cst

它最容易理解,也最严格。

常用内存序包括:

std::memory_order_relaxed
std::memory_order_acquire
std::memory_order_release
std::memory_order_acq_rel
std::memory_order_seq_cst

还有:

std::memory_order_consume

实践中通常不建议使用 consume,一般按 acquire 处理。

11. memory_order_relaxed

只保证原子对象本身的操作不可分割,不负责建立线程之间的同步关系。

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

counter.fetch_add(1, std::memory_order_relaxed);

适用于只关心计数结果、不依赖其他共享数据可见性的场景,例如统计请求次数:

std::atomic<unsigned long long> requests{0};

void handle_request()
{
    requests.fetch_add(1, std::memory_order_relaxed);

    // 处理请求
}

relaxed 仍然是原子的,不会丢失更新;只是它不会使其他普通变量的读写自动同步。

12. releaseacquire

这是最常见的线程间发布模式。

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

int data = 0;
std::atomic<bool> ready{false};

void producer()
{
    data = 42;

    ready.store(true, std::memory_order_release);
}

void consumer()
{
    while (!ready.load(std::memory_order_acquire)) {
    }

    std::cout << data << '\n'; // 安全地读到 42
}

发生关系是:

生产者写 data
      ↓
release 写 ready
      ↓ 同步
acquire 读 ready
      ↓
消费者读 data

只要消费者的 acquire 读取到了生产者 release 写入的值,生产者在 release 之前的修改就对消费者可见。

注意:data 虽然不是原子变量,但这里不会发生数据竞争,因为 release/acquire 建立了正确的同步关系。

如果改成:

ready.store(true, std::memory_order_relaxed);

和:

ready.load(std::memory_order_relaxed);

就不能用 ready 来保证 data 的安全发布。

13. memory_order_acq_rel

同时具有 acquire 和 release 语义,通常用于“读取并修改”操作:

value.fetch_add(1, std::memory_order_acq_rel);
value.exchange(10, std::memory_order_acq_rel);

含义是:

  • acquire:后面的操作不能越过当前操作提前
  • release:前面的操作不能越过当前操作推后

纯读取操作不能使用 release,纯写入操作不能使用 acquire

value.load(std::memory_order_acquire);
value.store(10, std::memory_order_release);

14. memory_order_seq_cst

这是默认选项:

value.store(10);
int x = value.load();

等价于:

value.store(10, std::memory_order_seq_cst);
int x = value.load(std::memory_order_seq_cst);

它除了拥有 acquire/release 的同步效果,还要求所有 seq_cst 原子操作表现得像存在一个全局一致顺序。

初学时建议:

先使用默认 seq_cst,只有经过性能测量和严格推理后,再使用更弱的内存序。

错误的内存序往往非常隐蔽,测试通过也不代表代码正确。

15. CAS 的两个内存序

比较交换可以指定成功与失败时不同的内存序:

value.compare_exchange_weak(
    expected,
    desired,
    std::memory_order_acq_rel, // 成功
    std::memory_order_acquire  // 失败
);

失败时没有执行写入,因此失败内存序不能是:

std::memory_order_release
std::memory_order_acq_rel

通常先使用默认内存序。如果确实需要优化,再仔细指定。

16. wait()notify_*():C++20

C++20 支持等待原子变量发生变化,避免持续忙循环。

忙等待写法

while (!ready.load()) {
    // 不停消耗 CPU
}

使用 wait

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

std::atomic<bool> ready{false};
int data = 0;

void consumer()
{
    ready.wait(false);

    std::cout << data << '\n';
}

int main()
{
    std::thread t(consumer);

    data = 42;
    ready.store(true);
    ready.notify_one();

    t.join();
}

ready.wait(false) 的含义是:

ready 仍然等于 false 时等待;发现它不等于 false 后返回。

通知方式:

ready.notify_one(); // 唤醒一个等待线程
ready.notify_all(); // 唤醒所有等待线程

常见模式:

state.store(newState, std::memory_order_release);
state.notify_all();

等待方:

state.wait(oldState, std::memory_order_acquire);

wait() 应关注的是“值是否改变”。即使发生通知,只要值仍然与参数相等,它也可以继续等待。

17. std::atomic_flag

std::atomic_flag 是最简单的原子标志类型。

std::atomic_flag flag = ATOMIC_FLAG_INIT;

C++20 也可以直接初始化:

std::atomic_flag flag{};

主要操作:

flag.test_and_set();
flag.clear();

简单自旋锁:

class SpinLock {
public:
    void lock()
    {
        while (flag_.test_and_set(std::memory_order_acquire)) {
        }
    }

    void unlock()
    {
        flag_.clear(std::memory_order_release);
    }

private:
    std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
};

使用方式:

SpinLock lock;

void work()
{
    lock.lock();

    // 临界区

    lock.unlock();
}

实际项目中通常优先使用:

std::mutex
std::lock_guard

自旋锁只适合临界区极短、线程不会长时间被挂起且经过性能分析的特殊场景。否则它会白白消耗 CPU。

18. 原子指针

std::atomic<T*> 支持原子地操作指针:

int values[10]{};
std::atomic<int*> ptr{values};

int* old = ptr.fetch_add(1);

这里的 fetch_add(1) 会移动一个 int 元素,而不是一个字节。

常见操作:

ptr.load();
ptr.store(newPtr);
ptr.exchange(newPtr);
ptr.compare_exchange_weak(expected, desired);

原子指针只保证指针本身的读写安全,并不自动保证指针所指向对象的生命周期和内容安全。

例如:

std::atomic<int*> ptr;

不能自动解决:

  • 对象何时销毁
  • 其他线程是否还持有旧指针
  • 指向对象的字段是否存在数据竞争

19. 原子智能指针

C++20 支持:

std::atomic<std::shared_ptr<T>>
std::atomic<std::weak_ptr<T>>

示例:

#include <atomic>
#include <memory>

struct Config {
    int timeout;
};

std::atomic<std::shared_ptr<const Config>> config{
    std::make_shared<const Config>(Config{100})
};

void update_config()
{
    auto next = std::make_shared<const Config>(Config{200});
    config.store(next, std::memory_order_release);
}

void read_config()
{
    auto current = config.load(std::memory_order_acquire);

    if (current) {
        int timeout = current->timeout;
    }
}

这种方式适合:

  • 配置对象整体替换
  • 多线程读取同一份不可变快照
  • 自动管理旧对象生命周期

这里推荐把对象设计为不可变,否则智能指针自身虽然原子,所指对象仍可能发生数据竞争。

20. std::atomic_ref:C++20

std::atomic_ref<T> 可以对已有普通对象进行原子访问:

#include <atomic>

int value = 0;

void add()
{
    std::atomic_ref<int> ref(value);
    ref.fetch_add(1);
}

它不会创建新对象,而是把现有对象视为原子对象操作。

注意事项:

  • 被引用对象必须满足对齐要求。
  • atomic_ref 存在期间,不能同时以普通非原子方式访问该对象。
  • 多个线程应当统一使用原子方式访问它。

错误示例:

int value = 0;
std::atomic_ref<int> ref(value);

ref.fetch_add(1);
value++; // 如果同时有其他线程操作,可能产生数据竞争

可以检查对齐要求:

std::atomic_ref<int>::required_alignment

21. 原子操作不能组合成原子事务

下面每个操作单独都是原子的:

std::atomic<int> balance{100};

if (balance.load() >= 80) {
    balance.fetch_sub(80);
}

但整体不是原子的。两个线程可能同时读到 100,然后都扣除 80,最终变成 -60

可以使用 CAS 循环:

bool withdraw(std::atomic<int>& balance, int amount)
{
    int current = balance.load();

    while (current >= amount) {
        if (balance.compare_exchange_weak(
                current,
                current - amount)) {
            return true;
        }
    }

    return false;
}

如果逻辑涉及多个变量或复杂不变量,通常应该使用互斥锁:

#include <mutex>

std::mutex mutex;
int balance = 100;

bool withdraw(int amount)
{
    std::lock_guard<std::mutex> guard(mutex);

    if (balance < amount) {
        return false;
    }

    balance -= amount;
    return true;
}

22. volatile 不能替代 atomic

错误认识:

volatile int counter = 0;

volatile 不保证:

  • 多线程原子性
  • 线程间同步
  • 内存可见性
  • 不发生数据竞争

它主要用于特殊内存访问场景,例如某些硬件寄存器,而不是常规的线程同步。

多线程共享数据应该使用:

std::atomic
std::mutex

23. atomicmutex 如何选择

适合使用 std::atomic

  • 简单计数器
  • 独立布尔状态
  • 指针或快照整体替换
  • 引用计数
  • 无锁数据结构中的基础操作

适合使用 std::mutex

  • 多个变量必须保持一致
  • 一次操作包含多个步骤
  • 需要保护容器或复杂对象
  • 临界区逻辑较复杂
  • 可读性和正确性比极限性能更重要

例如,不能因为状态字段是原子类型,就认为整个对象安全:

struct Account {
    std::atomic<int> state;
    int balance;
    std::string owner;
};

state 是线程安全的,不代表 balanceowner 也安全。

24. 常见错误总结

错误一:先读再写

atomicValue.store(atomicValue.load() + 1);

两个线程可能读到相同旧值,导致更新丢失。应该使用:

atomicValue.fetch_add(1);

错误二:认为原子对象之间整体原子

std::atomic<int> x{0};
std::atomic<int> y{0};

x.store(1);
y.store(1);

只能保证两次写入分别原子,不能保证其他线程把它们当作一个整体观察。

错误三:用 relaxed 发布普通数据

data = 42;
ready.store(true, std::memory_order_relaxed);

如果 ready 是用来通知其他线程读取 data,应该使用 release/acquire。

错误四:原子指针指向普通可变对象

std::atomic<MyObject*> ptr;

它只保证 ptr 安全,不保证 *ptr 安全。

错误五:无依据地使用复杂内存序

较弱的内存序并不一定带来明显性能提升,却会显著增加证明代码正确性的难度。默认从 seq_cst 开始通常更稳妥。

25. 一个实用的停止标志示例

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

std::atomic<bool> stopped{false};

void worker()
{
    while (!stopped.load(std::memory_order_relaxed)) {
        std::cout << "working...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

int main()
{
    std::thread thread(worker);

    std::this_thread::sleep_for(std::chrono::seconds(1));

    stopped.store(true, std::memory_order_relaxed);
    thread.join();
}

这里只有一个独立停止标志,不依赖其他数据的发布,因此 relaxed 通常足够。

最后可以这样记:

  • load/store:原子读写
  • fetch_add/fetch_sub:原子增减
  • exchange:无条件替换
  • compare_exchange_*:满足条件才替换
  • wait/notify:等待和通知
  • relaxed:只要原子性
  • release/acquire:发布数据与获取数据
  • seq_cst:最直观、默认、最容易保证正确
  • 多变量复杂逻辑:优先考虑 std::mutex