Wiki
std::atomic
std::atomic<T> 用来对一个共享对象执行原子操作,定义在:
#include <atomic>
“原子”表示某个操作在并发观察下不可被拆成一半看到。例如一个原子自增不会出现另一个线程只看到“读取完成但写回还没完成”的中间状态。
但要特别注意:
std::atomic保证原子语义,不等于保证底层实现一定 lock-free。
是否真正无锁取决于类型、平台和标准库实现。
1.为什么普通变量不够
下面的代码存在数据竞争:
int counter = 0;
void add_many()
{
for (int i = 0; i < 100000; ++i)
{
++counter;
}
}
如果多个线程同时执行 ++counter,结果并不可靠。
对于这种“单个独立计数器”的情况,可以使用:
std::atomic<int> counter{0};
2.最基础的 load() 和 store()
原子对象可以显式读取和写入:
std::atomic<int> value{0};
value.store(10);
int x = value.load();
很多简单场景也允许写成类似普通变量的形式:
value = 10;
int x = value;
但在学习原子语义时,显式写 load() / store() 更容易看出这里发生的是原子访问。
3.原子自增与 fetch_add()
#include <atomic>
#include <iostream>
#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::cout << counter.load() << '\n';
}
还可以显式使用:
counter.fetch_add(1);
常见原子读改写操作包括:
fetch_add()
fetch_sub()
fetch_and()
fetch_or()
fetch_xor()
其中位运算版本主要用于整数原子类型。
4.exchange()
exchange() 会原子地:
- 写入新值;
- 返回旧值。
std::atomic<int> value{10};
int old = value.exchange(20);
执行后:
old == 10
value == 20
这种“替换并取回旧值”的操作在状态切换里很常见。
5.std::atomic<bool>:状态标记
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
std::atomic<bool> running{true};
void worker()
{
while (running.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "stopped\n";
}
int main()
{
std::thread t(worker);
std::this_thread::sleep_for(std::chrono::milliseconds(350));
running.store(false);
t.join();
}
这种简单停止标记很适合原子变量。
但如果使用 C++20,还应该了解后面的 std::jthread 和 std::stop_token,因为它们提供了更结构化的协作式停止机制。
6.atomic 不等于“整个对象线程安全”
如果一个类有多个字段:
std::atomic<int> x;
std::atomic<int> y;
只能保证对 x 和 y 各自的原子操作不会撕裂。
它并不会自动保证:
x 和 y 永远表示同一个逻辑状态
例如:
x.store(10);
y.store(20);
另一个线程完全可能在两次写入之间观察到:
x == 10
y == 旧值
如果多个变量必须作为整体保持一致,通常应该使用同一把 mutex 保护整个状态。
7.compare_exchange:比较并交换(CAS)
CAS 是很多无锁算法的核心操作。
概念上它做的是:
如果 atomic 当前值 == expected
atomic = desired
返回成功
否则
expected = atomic 当前值
返回失败
C++ 提供:
compare_exchange_weak()
compare_exchange_strong()
示例:
#include <atomic>
#include <iostream>
int main()
{
std::atomic<int> value{10};
int expected = 10;
bool success = value.compare_exchange_strong(expected, 20);
std::cout << std::boolalpha << success << '\n';
std::cout << value.load() << '\n';
}
输出:
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
7.1.weak 和 strong
compare_exchange_weak() 允许“伪失败”:即使值相等,也可能返回失败。
因此它很适合放在循环中:
int expected = value.load();
while (!value.compare_exchange_weak(expected, expected + 1))
{
}
compare_exchange_strong() 不允许这种伪失败,更适合只尝试一次的场景。
8.std::atomic_flag
std::atomic_flag 是一种非常基础的原子标志类型。
它常见的两个操作是:
test_and_set()
clear()
可以用来实现最简单的自旋锁思想:
std::atomic_flag flag = ATOMIC_FLAG_INIT;
while (flag.test_and_set())
{
// 自旋等待
}
// 临界区
flag.clear();
但普通业务代码不要因为“看起来无锁”就自己发明自旋锁。自旋会持续占用 CPU,在很多场景下反而不如标准互斥量合适。
9.volatile 不能替代 atomic
这是非常常见的误区。
volatile bool running;
不能用来解决 C++ 多线程同步问题。
volatile 主要表达的是:
这个对象的访问具有特殊可观察意义,编译器不能像普通对象那样随意消除相关访问。
它不提供:
- 原子性;
- 跨线程同步;
- happens-before 关系;
- 数据竞争保护。
线程间共享状态应使用 std::atomic、mutex 或其他标准同步工具。
10.内存序:为什么 atomic 还有第二个参数
你会看到:
value.store(10, std::memory_order_release);
value.load(std::memory_order_acquire);
这个第二个参数控制的是 内存顺序(memory ordering)。
原子操作不只解决“这个变量本身是否原子”,还可能承担不同线程之间建立可见性顺序的作用。
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。
11.默认内存序:seq_cst
如果不写第二个参数:
value.store(10);
int x = value.load();
默认使用:
std::memory_order_seq_cst
它提供最直观、最强的一致性模型,也是最适合初学和默认使用的选择。
除非你已经能够证明更弱的内存序是正确且有必要的,否则不要为了“优化”随便改成 relaxed。
12.memory_order_relaxed
relaxed 只保证这个原子对象本身的操作是原子的,不额外建立其他普通内存访问的同步顺序。
简单独立统计计数常常可以使用:
counter.fetch_add(1, std::memory_order_relaxed);
因为我们只关心:
最终计数不要丢。
并不依赖这个计数器来发布其他数据。
13.acquire / release 的基本思想
一个常见模型是:
线程 A 写普通数据
↓
release 写入一个原子标志
↓
线程 B acquire 读到这个标志
↓
线程 B 可以看到此前被正确发布的数据
例如:
#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';
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
这里 data 本身不是 atomic,但 release/acquire 通过 ready 建立了同步关系。
这一部分属于并发中的进阶内容。初学阶段最重要的是:
默认先用
seq_cst,只有在明确理解内存模型和性能需求时才主动使用更弱的内存序。
14.is_lock_free()
可以查询某个原子对象的实现是否 lock-free:
std::atomic<int> value{0};
bool lock_free = value.is_lock_free();
C++20 以后还可以使用类型级常量:
std::atomic<int>::is_always_lock_free
因此不要把:
std::atomic<T>
简单理解成:
一定由 CPU 的无锁指令实现
它保证的是语义,而不是某一种具体实现方式。
15.C++20:wait() / notify_one() / notify_all()
C++20 为原子对象增加了直接等待值变化的能力。
例如:
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> ready{false};
void worker()
{
ready.wait(false);
std::cout << "start\n";
}
int main()
{
std::thread t(worker);
ready.store(true);
ready.notify_one();
t.join();
}
相比:
while (!ready.load())
{
}
wait() 不需要一直忙等占用 CPU。
对于“只围绕一个原子值等待”的简单场景,这是一种很方便的工具。
16.atomic 和 mutex 怎么选
| 场景 | 推荐 |
|---|---|
| 简单计数器 | std::atomic |
| 简单布尔状态 | std::atomic<bool> |
| 一个指针或整数的独立状态切换 | std::atomic |
| 多个字段必须一起保持一致 | std::mutex |
修改 vector、map 等复杂容器 |
通常用 std::mutex |
| 复杂业务逻辑临界区 | std::mutex |
| 等待复杂条件 | std::condition_variable + mutex |
不要为了追求“无锁”而强行把一个本来适合 mutex 的复合状态拆成很多 atomic。
代码正确性和可维护性通常比理论上的锁开销更重要。
17.常见错误
- 把
std::atomic理解成“一定 lock-free”。 - 把
volatile当作线程同步工具。 - 用多个 atomic 表达一个必须整体一致的状态,却没有额外同步。
- 在不理解内存模型时随意把所有操作改成
memory_order_relaxed。 - 自己实现自旋锁并在临界区长时间工作,导致 CPU 空转。
- 认为“atomic 变量安全”就等于“包含它的整个类都线程安全”。
18.小结
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可以避免简单状态等待时的忙轮询。