Wiki
std::atomic
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() 会原子地:
- 写入新值;
- 返回旧值。
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::jthread 和 std::stop_token,因为它们提供了更结构化的协作式停止机制。
1.6.atomic 不等于“整个对象线程安全”
如果一个类有多个字段:
std::atomic<int> x;
std::atomic<int> y;
只能保证对 x 和 y 各自的原子操作不会撕裂。
它并不会自动保证:
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.weak 和 strong
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();
这个操作会原子地完成两件事:
- 返回
flag原来的值; - 同时把
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::atomic、mutex 或其他标准同步工具。
可以粗略记成:
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;只有在明确理解同步关系,并且确实有必要时,再主动使用relaxed、acquire、release等更弱的内存序。
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 |
修改 vector、map 等复杂容器 |
通常用 std::mutex |
| 复杂业务逻辑临界区 | std::mutex |
| 等待复杂条件 | std::condition_variable + mutex |
不要为了追求“无锁”而强行把一个本来适合 mutex 的复合状态拆成很多 atomic。
代码正确性和可维护性通常比理论上的锁开销更重要。
一个实用判断是:
能用一句话说清楚“这个 atomic 独立表示什么状态”
→ 可以考虑 atomic
需要解释好几个字段之间必须如何配合
→ 通常先考虑 mutex
5.2.常见错误
- 把
std::atomic理解成“一定 lock-free”。 - 把
volatile当作线程同步工具。 - 用多个 atomic 表达一个必须整体一致的状态,却没有额外同步。
- 在不理解内存模型时随意把所有操作改成
memory_order_relaxed。 - 自己实现自旋锁并在临界区长时间工作,导致 CPU 空转。
- 认为“atomic 变量安全”就等于“包含它的整个类都线程安全”。
6.小结
std::atomic<T>保证针对该对象的原子操作语义,但不保证所有实现一定 lock-free。load()/store()负责原子读写;fetch_add()等负责原子读改写。exchange()可以原子替换并获得旧值。compare_exchange_weak/strong()是 CAS 操作,也是很多无锁算法的基础。volatile不能替代atomic。- 默认内存序是
memory_order_seq_cst,初学阶段优先使用默认值。 relaxed、acquire、release属于更深入的内存模型内容,必须在理解同步关系后再使用。- C++20 的
atomic::wait/notify可以避免简单状态等待时的忙轮询。