跳到正文

Wiki

std::atomic

约 15 分钟阅读

std::atomic<T> 用来对一个共享对象执行原子操作,定义在:

#include <atomic>

“原子”表示某个操作在并发观察下不可被拆成一半看到。 例如一个原子自增不会出现另一个线程只看到“读取完成但写回还没完成”的中间状态。

但要特别注意:

std::atomic 保证原子语义,不等于保证底层实现一定 lock-free。

是否真正无锁取决于类型、平台和标准库实现。

学习 atomic 时不要一上来就想“无锁算法”。

更适合初学的顺序是:

普通变量为什么会出错

单个变量怎样做到原子读写

读-改-写怎样作为一个整体完成

多个线程之间怎样发布和观察状态

什么时候 atomic 不够,需要 mutex

也就是说,std::atomic 首先是一个正确性工具,其次才可能是性能工具。

1.atomic 解决什么问题

1.1.为什么普通变量不够

下面的代码存在数据竞争:

int counter = 0;

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

如果多个线程同时执行 ++counter,结果并不可靠。

原因不是 int 本身不能存数字,而是:

++counter;

看起来是一句代码,实际通常可以拆成:

读取 counter

加 1

写回 counter

两个线程交错执行时,就可能出现:

线程 A 读到 10
线程 B 读到 10
线程 A 写回 11
线程 B 也写回 11

明明执行了两次自增,结果只增加了 1。

更重要的是,在 C++ 里这不只是“结果偶尔不准”,而是 数据竞争,属于未定义行为。

对于这种“单个独立计数器”的情况,可以使用:

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

1.2.最基础的 load()store()

原子对象可以显式读取和写入:

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

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

很多简单场景也允许写成类似普通变量的形式:

value = 10;
int x = value;

但在学习原子语义时,显式写 load() / store() 更容易看出这里发生的是原子访问。

可以把它们理解成:

load()  :从 atomic 对象中原子地读出一个值
store() :向 atomic 对象中原子地写入一个值

这里“原子”只针对这一次读或写。

例如:

int a = value.load();
int b = other.load();

这两次 load() 各自是原子的,但它们并不会自动组成一个“同时读取两个值”的整体事务。 想要同时读取俩,还是用mutex方便点。

1.3.原子自增与 fetch_add()

自增、自减这类操作叫 读-改-写(read-modify-write, RMW)

它们不是简单读,也不是简单写,而是:

读出旧值

计算新值

写回新值

atomic 的关键价值之一,就是能把这整个过程做成一个不可被其他线程插进来的原子操作。

#include <atomic>
#include <print>
#include <thread>
#include <vector>

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

void add_many()
{
    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(add_many);
    }

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

    std::println("{}", counter.load());
}

运行结果:

400000

还可以显式使用:

counter.fetch_add(1);

常见原子读改写操作包括:

fetch_add()
fetch_sub()
fetch_and()
fetch_or()
fetch_xor()

其中位运算版本主要用于整数原子类型。

fetch_add()++counter 的区别是:

++counter;

更接近“把值加一,并得到加完之后的新值”;

counter.fetch_add(1);

更明确表达“原子地加 1”,并返回加之前的旧值。

例如:

int old = counter.fetch_add(1);

如果 old == 10,说明这次操作把 counter 从 10 改成了 11

1.4.exchange()

exchange() 会原子地:

  1. 写入新值;
  2. 返回旧值。
std::atomic<int> value{10};

int old = value.exchange(20);

执行后:

old == 10
value == 20

这种“替换并取回旧值”的操作在状态切换里很常见。

例如一个任务状态从:

idle

切换成:

running

同时还想知道切换前到底是什么状态,就可以使用 exchange()

它比:

int old = value.load();
value.store(20);

更强,因为后者中间可能被其他线程插入修改。

1.5.std::atomic<bool>:状态标记

#include <atomic>
#include <chrono>
#include <print>
#include <thread>

std::atomic<bool> running{true};

void worker()
{
    while (running.load())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    std::println("stopped");
}

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

    std::this_thread::sleep_for(std::chrono::milliseconds(350));
    running.store(false);

    t.join();
}

运行结果:

stopped

这种简单停止标记很适合原子变量。

这里的 running 只表达一个独立事实:

线程是否继续循环

它不需要和其他多个字段一起保持复杂不变量,所以用 atomic<bool> 很合适。

如果停止时还要同时修改很多共享状态,例如:

running = false
queue.clear()
error_code = xxx

那就不能只因为其中一个字段是 atomic,就认为整个停止过程自动安全。此时通常要重新考虑 mutex 或更完整的停止协议。

mutex可以制造一大段的临界区,而atomic不能,所以这种要好几行临界的就用mutex

但如果使用 C++20,还应该了解后面的 std::jthreadstd::stop_token,因为它们提供了更结构化的协作式停止机制。

1.6.atomic 不等于“整个对象线程安全”

如果一个类有多个字段:

std::atomic<int> x;
std::atomic<int> y;

只能保证对 xy 各自的原子操作不会撕裂。

它并不会自动保证:

x 和 y 永远表示同一个逻辑状态

例如:

x.store(10);
y.store(20);

另一个线程完全可能在两次写入之间观察到:

x == 10
y == 旧值

如果多个变量必须作为整体保持一致,通常应该使用同一把 mutex 保护整个状态。

可以用一句话记:

atomic 保护的是“一个对象的一次操作”,mutex 更适合保护“一组状态的不变量”。

例如银行账户里同时有:

balance
last_update_time
version

如果这三个值必须一起变化,那么把它们分别做成 atomic 并不一定让设计更清楚。

2.CAS 与原子标志

2.1.compare_exchange:比较并交换(CAS)

CAS 是很多无锁算法的核心操作。

不过初学时不要先把它想得太神秘。

CAS 最核心的语义就是:

只有当值还是我以为的那个旧值时,才把它改成新值。

概念上它做的是:

如果 atomic 当前值 == expected
    atomic = desired
    返回成功
否则
    expected = atomic 当前值
    返回失败

C++ 提供:

compare_exchange_weak()
compare_exchange_strong()

示例:

#include <atomic>
#include <print>

int main()
{
    std::atomic<int> value{10};

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

    std::println("{}", success);
    std::println("{}", value.load());
}

运行结果:

true
20

如果 expected 不匹配:

std::atomic<int> value{10};
int expected = 5;

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

那么:

success == false
value 仍然是 10
expected 被更新成 10

这里 expected 会被改写,是 CAS 很重要的细节。

它不是单纯的“输入参数”,失败时它会告诉你:

你猜的旧值不对,当前真实值是这个

所以 CAS 循环通常会写成:

先读当前值作为 expected

根据 expected 算 desired

尝试 compare_exchange

失败时 expected 已被更新,重新计算 desired

2.1.1.weakstrong

compare_exchange_weak() 允许“伪失败”:即使值相等,也可能返回失败。

因此它很适合放在循环中:

int expected = value.load();

while (!value.compare_exchange_weak(expected, expected + 1))
{
}

这段代码能工作,是因为失败时 expected 会被更新成 atomic 当前值。

下一轮循环中的:

expected + 1

就会基于更新后的值继续尝试。

compare_exchange_strong() 不允许这种伪失败,更适合只尝试一次的场景。

简单记法:

循环里反复尝试
→ weak 常见

只想尝试一次
→ strong 更直观

2.2.std::atomic_flag

std::atomic_flag 可以理解成一个非常简单的原子开关

它只有两种状态:

false:空闲
true :已占用

它常见的两个操作是:

test_and_set()
clear()

其中最关键的是:

flag.test_and_set();

这个操作会原子地完成两件事

  1. 返回 flag 原来的值;
  2. 同时把 flag 设置为 true

例如:

std::atomic_flag flag = ATOMIC_FLAG_INIT;

初始时可以理解为:

flag = false

第一次执行:

bool old = flag.test_and_set();

结果相当于:

old = false
flag = true

如果这时候另一个线程再次执行:

bool old = flag.test_and_set();

那么结果就是:

old = true
flag = true

因此,可以利用它实现一个最简单的自旋锁:

std::atomic_flag flag = ATOMIC_FLAG_INIT;

while (flag.test_and_set())
{
    // 自旋等待
}

// 临界区

flag.clear();

这段代码的逻辑是:

flag == false

线程执行 test_and_set()

返回 false,同时把 flag 改为 true

while(false)

成功进入临界区

如果另一个线程这时候也来执行:

flag.test_and_set();

由于 flag 已经是 true,所以它会返回 true

while (true)
{
}

于是这个线程会不断重复执行:

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

直到持有锁的线程执行:

flag.clear();

把:

flag = true

重新变成:

flag = false

等待中的线程下一次执行 test_and_set() 时,就可以成功获得这个“锁”。

这种不断循环检查锁有没有释放的行为,就叫做:

自旋(spin)

可以把它理解成:

一个线程发现锁被别人占用了之后,不睡眠,也不阻塞,而是一直站在那里反复询问:“锁释放了吗?”

所以自旋锁和普通 std::mutex 的区别之一就在这里。

普通互斥量在等待时间较长时,线程通常可以被操作系统阻塞或挂起,避免持续占用 CPU。

而自旋锁:

while (flag.test_and_set())
{
}

会一直执行循环,因此可能持续占用 CPU。

例如:

while (flag.test_and_set())
{
}

// 临界区

read_file();
send_network();
sleep();
do_heavy_work();

flag.clear();

如果临界区执行时间很长,其他线程就会一直在外面自旋等待,白白浪费 CPU。

因此,自旋锁更适合临界区非常短的情况,例如:

while (flag.test_and_set())
{
}

++value;

flag.clear();

如果锁很快就会被释放,那么线程稍微自旋几次,可能比让操作系统把线程挂起、再重新唤醒的开销更低。

但在普通业务代码中,一般不要因为自旋锁“看起来简单”就自己使用 std::atomic_flag 实现锁。

如果临界区中存在:

IO

sleep

网络请求

文件操作

复杂计算

可能阻塞的函数

通常更适合使用:

std::mutex

配合:

std::lock_guard
std::scoped_lock
std::unique_lock

来进行同步。

std::atomic_flag 真正重要的地方在于:

test_and_set()

中的:

读取旧值 + 设置为 true

是一个不可分割的原子操作。

如果使用普通 bool

bool flag = false;

两个线程可能同时发生:

线程 A:看到 flag == false
线程 B:也看到 flag == false

线程 A:flag = true
线程 B:flag = true

线程 A:认为自己获得了锁
线程 B:也认为自己获得了锁

这样两个线程就可能同时进入临界区。

std::atomic_flag::test_and_set() 可以保证多个线程同时竞争时,只有一个线程能够看到原来的值是 false,从而成功进入临界区。

因此,可以把 std::atomic_flag 简单理解为:

std::atomic_flag 是一个原子的“占用标志”。test_and_set() 会原子地读取旧值并把标志设置为 true,因此可以用它实现最简单的自旋锁;没有抢到锁的线程会不断循环检查,这种行为就叫自旋。

2.3.volatile 不能替代 atomic

这是非常常见的误区。

volatile bool running;

不能用来解决 C++ 多线程同步问题。

volatile 主要表达的是:

这个对象的访问具有特殊可观察意义,编译器不能像普通对象那样随意消除相关访问。

它不提供:

  • 原子性;
  • 跨线程同步;
  • happens-before 关系;
  • 数据竞争保护。

线程间共享状态应使用 std::atomicmutex 或其他标准同步工具。

可以粗略记成:

volatile:主要解决“编译器不要省略这次访问”
atomic  :解决“多线程下这次访问如何正确同步”

它们不是同一类工具。

3.内存序

3.1.为什么 atomic 还有第二个参数

你会看到:

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

int x = value.load(std::memory_order_acquire);

这里的第二个参数控制的是:

内存顺序(memory ordering)

学习内存序时,最重要的是先区分两个概念:

原子性



内存访问之间的顺序

原子性解决的是:

这个 atomic 对象本身的操作是不是不可分割的?

例如:

counter.fetch_add(1);

多个线程同时执行时,不会因为普通的“读取 → 加一 → 写回”互相覆盖而把计数加丢。

而内存序解决的是:

这个 atomic 操作前后的其他内存读写,应该和其他线程建立什么样的顺序关系?

例如:

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

线程 A:

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

线程 B:

while (!ready.load(...))
{
}

std::println("{}", data);

这里我们不只关心:

ready 能不能正确读到 true

还关心:

线程 B 看到 ready == true 以后,

能不能保证看到线程 A 之前写好的 data = 42。

这就是内存序要解决的问题。

C++ 常见的内存序包括:

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


3.2.默认内存序:seq_cst

如果不写第二个参数:

value.store(10);

int x = value.load();

默认使用:

std::memory_order_seq_cst

它是最强、也最容易理解的常用内存序。

可以先粗略理解成:

所有线程看到这些原子操作时,效果尽量像它们都处在一个大家一致认可的全局顺序中。

所以初学阶段:

不知道应该用什么
→ 直接使用默认的 seq_cst

通常是最稳妥的选择。

不要为了所谓“性能优化”,看到:

std::memory_order_relaxed

就把所有原子操作都改成 relaxed

内存序写错以后,程序可能平时运行完全正常,但换个平台、编译器或优化级别以后才出现非常难排查的并发问题。


3.3.memory_order_relaxed

relaxed 只要求:

这个 atomic 对象本身的操作仍然是原子的。

例如:

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

多个线程同时执行时,计数仍然不会因为并发更新而丢失。

所以像这种:

独立统计次数

请求计数

事件发生次数

通常可以考虑 relaxed

因为我们只关心:

counter 自己最后是多少

并不需要通过这个 counter 去同步其他普通变量。

但要注意:

relaxed 只保证 atomic 自己的访问,不会顺便让旁边的普通变量变得线程安全。

例如:

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

void producer()
{
    data = 42;

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

另一个线程即使:

ready.load(std::memory_order_relaxed)

读到了 true,也不能单纯依靠这个 relaxed 操作建立:

data = 42



读取 data

之间所需要的跨线程同步关系。


3.4.acquire / release

acquire / release 最经典的用途是:

一个线程先准备数据,然后通过 atomic 发布“数据已经准备好了”;另一个线程看到这个信号以后再读取数据。

例如:

#include <atomic>
#include <print>
#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::println("{}", data);
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();
}

运行结果:

42

可以这样理解:

producer:

data = 42

先把普通数据准备好

release store

发布 ready = true

consumer:

acquire load

看到 ready == true

接收到 producer 发布的数据

读取 data

也就是:

写普通数据



release



acquire



读普通数据

这里 ready 本身只是一个同步信号。

真正重要的是:

producer 在 release 之前完成的操作,可以通过对应的 acquire 与 consumer 建立同步关系。

所以可以简单记成:

release
→ 发布之前完成的工作

acquire
→ 接收对方发布的工作

3.5.memory_order_acq_rel

有些原子操作既会读取旧值,又会写入新值,例如:

fetch_add()
exchange()
compare_exchange()

这种操作有时同时需要:

acquire

+

release

于是可以使用:

std::memory_order_acq_rel

可以先简单理解成:

既接收别人之前发布的数据

又把自己之前完成的操作继续发布出去

这一部分更多出现在复杂的无锁数据结构中,初学阶段知道它的含义即可。


最重要的是记住:

原子性
→ atomic 自己的操作是否不可分割

内存序
→ atomic 操作和其他内存访问之间建立怎样的跨线程顺序

以及一个实用原则:

默认先用 seq_cst;只有在明确理解同步关系,并且确实有必要时,再主动使用 relaxedacquirerelease 等更弱的内存序。

4.等待与实现特性

4.1.is_lock_free()

std::atomic<T> 保证的是:

对这个对象的原子操作具有标准规定的线程安全语义。

但它不保证底层一定完全不用锁

例如:

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

bool lock_free = value.is_lock_free();

is_lock_free() 用来查询:

当前这个原子对象,在当前平台和当前实现中,底层操作是否可以不依赖额外的锁来完成。

如果返回:

true

说明它的原子操作是 lock-free 的,也就是底层通常可以直接依靠 CPU 提供的原子指令完成。

如果返回:

false

说明标准库为了实现这个原子操作,内部可能需要借助某种锁或其他同步机制。

例如:

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

std::println("{}", value.is_lock_free());

在常见的 x86-64、ARM 等平台上,像:

std::atomic<int>
std::atomic<bool>
std::atomic<int*>

这种比较小的类型通常都是 lock-free 的。

但是对于比较大的类型,情况就不一定了。

例如:

struct Data
{
    long long a;
    long long b;
};

std::atomic<Data> data;

这种对象比较大,CPU 可能没有办法用一条或少量原子指令直接完成操作。

此时标准库仍然可以提供:

data.load();
data.store(...);

这样的原子语义,但内部可能需要通过锁来实现。

因此:

std::atomic<T>

应该理解成:

保证“操作是原子的”。

而不是:

保证“实现一定无锁”。


还可以使用:

std::atomic<int>::is_always_lock_free

它是一个编译期常量,用来判断:

这个 std::atomic<int> 类型在当前实现中是否永远都是 lock-free 的。

例如:

if constexpr (std::atomic<int>::is_always_lock_free)
{
    // 当前实现保证 atomic<int> 永远是 lock-free
}

可以简单区分成:

value.is_lock_free()

表示:

这个具体 atomic 对象现在是不是 lock-free。

而:

std::atomic<T>::is_always_lock_free

表示:

这个 atomic 类型在当前实现中是不是始终保证 lock-free。

所以最重要的一点是:

atomic ≠ 一定无锁

atomic 真正表示的是:

原子操作语义

也就是多个线程同时访问时,这些操作不会被拆成一半让其他线程插进来。

至于底层到底是:

CPU 原子指令

还是:

标准库内部借助锁实现

这是具体平台和标准库实现决定的。

因此可以记成一句话:

std::atomic<T> 保证的是“原子性”,不保证“一定无锁”;is_lock_free() 就是用来查询它底层是否真正采用无锁实现的。

4.2.C++20:wait() / notify_one() / notify_all()

C++20 给原子类型增加了:

wait()
notify_one()
notify_all()

它们可以让线程等待某个原子变量的值发生变化,而不需要自己一直写循环忙等。

例如:

#include <atomic>
#include <print>
#include <thread>

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

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

    std::println("start");
}

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

    ready.store(true);
    ready.notify_one();

    t.join();
}

运行结果:

start

这里最关键的是:

ready.wait(false);

它的意思不是

等待 ready 变成 false

而是:

如果 ready 当前仍然等于 false,就等待;直到它不再等于 false 才返回。

所以执行过程可以理解成:

初始:

ready = false

worker 执行:

ready.wait(false);

发现:

ready == false

也就是当前值和传进去的旧值 false 相同,于是开始等待。

随后 main 执行:

ready.store(true);

把:

ready = false

改成:

ready = true

然后:

ready.notify_one();

通知一个正在等待 ready 的线程:

这个值可能发生变化了,你可以重新检查一下。

worker 被唤醒后发现:

ready != false

于是:

ready.wait(false);

返回,继续向下执行:

std::println("start");

所以整个过程可以画成:

worker                         main
   |                            |
   | ready.wait(false)          |
   |                            |
   | ready == false             |
   | 开始等待                   |
   |                            |
   |                    ready.store(true)
   |                            |
   |                    ready.notify_one()
   |                            |
   | <------ 被唤醒 ------------|
   |
   | ready != false
   |
   | wait() 返回
   |
   | println("start")

如果不用 wait(),以前可能会写成:

while (!ready.load())
{
}

这个循环会不断执行:

ready.load();
ready.load();
ready.load();
ready.load();
...

也就是线程一直占着 CPU 检查:

变成 true 了吗?

这种方式叫做忙等 / 自旋等待

而:

ready.wait(false);

在需要等待较长时间时,可以让线程阻塞等待,不需要一直空转消耗 CPU。

因此:

while (!ready.load())
{
}

可以理解成:

我一直盯着门看它有没有开。

而:

ready.wait(false);

更像:

门没开我就先睡觉,等有人叫我再起来看看。


4.3.notify_one()notify_all()

如果只有一个等待线程,可以:

ready.notify_one();

表示:

唤醒一个等待这个原子变量的线程。

如果有多个线程:

void worker()
{
    ready.wait(false);
    std::println("start");
}

然后创建:

std::thread t1(worker);
std::thread t2(worker);
std::thread t3(worker);

如果希望全部线程都被唤醒,可以:

ready.store(true);
ready.notify_all();

也就是:

notify_one()  → 通知一个等待线程

notify_all()  → 通知所有等待线程

4.4.wait() 到底在等什么?

记住这个规则就够了:

atomic.wait(old_value);

意思是:

只要当前值仍然等于 old_value,就继续等待;一旦当前值不等于 old_value,就返回。

例如:

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

value.wait(10);

表示:

value == 10

继续等待

如果其他线程执行:

value.store(20);
value.notify_one();

那么:

value != 10

于是 wait(10) 返回。

所以:

wait(x)

不要理解成:

等到值变成 x

而应该理解成:

值还是 x 的时候就等,等它不是 x 了再继续。


4.5.condition_variable 有什么区别? (学完condition_variable再回来看)

如果你的需求非常简单:

ready 从 false 变成 true

state 从 0 变成其他值

counter 不再等于某个旧值

直接:

atomic.wait()
atomic.notify_one()
atomic.notify_all()

会非常方便。

但是如果你的等待条件比较复杂,例如:

队列非空
或者 finished == true
或者 error != 0

这种条件涉及多个变量和共享数据:

queue.empty()
finished
error

通常还是使用:

std::condition_variable
+
std::mutex
+
谓词

更加清楚。

因此可以简单记成:

atomic::wait() 适合等待“一个原子变量的值发生变化”;condition_variable 更适合等待“复杂条件成立”。

最后把 wait() 的核心记住就行:

value.wait(x);

等价于一句人话:

“只要 value 还是 x,我就继续等;什么时候不是 x 了,我什么时候继续执行。”

5.选择与常见错误

5.1.atomic 和 mutex 怎么选

场景 推荐
简单计数器 std::atomic
简单布尔状态 std::atomic<bool>
一个指针或整数的独立状态切换 std::atomic
多个字段必须一起保持一致 std::mutex
修改 vectormap 等复杂容器 通常用 std::mutex
复杂业务逻辑临界区 std::mutex
等待复杂条件 std::condition_variable + mutex

不要为了追求“无锁”而强行把一个本来适合 mutex 的复合状态拆成很多 atomic。

代码正确性和可维护性通常比理论上的锁开销更重要。

一个实用判断是:

能用一句话说清楚“这个 atomic 独立表示什么状态”
→ 可以考虑 atomic

需要解释好几个字段之间必须如何配合
→ 通常先考虑 mutex

5.2.常见错误

  1. std::atomic 理解成“一定 lock-free”。
  2. volatile 当作线程同步工具。
  3. 用多个 atomic 表达一个必须整体一致的状态,却没有额外同步。
  4. 在不理解内存模型时随意把所有操作改成 memory_order_relaxed
  5. 自己实现自旋锁并在临界区长时间工作,导致 CPU 空转。
  6. 认为“atomic 变量安全”就等于“包含它的整个类都线程安全”。

6.小结

  • std::atomic<T> 保证针对该对象的原子操作语义,但不保证所有实现一定 lock-free。
  • load() / store() 负责原子读写;fetch_add() 等负责原子读改写。
  • exchange() 可以原子替换并获得旧值。
  • compare_exchange_weak/strong() 是 CAS 操作,也是很多无锁算法的基础。
  • volatile 不能替代 atomic
  • 默认内存序是 memory_order_seq_cst,初学阶段优先使用默认值。
  • relaxedacquirerelease 属于更深入的内存模型内容,必须在理解同步关系后再使用。
  • C++20 的 atomic::wait/notify 可以避免简单状态等待时的忙轮询。