C++原子操作是无锁同步的核心机制,用于多线程环境下对共享数据的不可中断操作,能够避免竞争条件,是替代传统互斥锁(std::mutex)实现更高效并发控制的现代基石。原子操作由 <atomic> 头文件提供,覆盖基本数据类型(如 int、bool)和自定义类型(C++20起)。
竞争条件与原子性
竞争条件
当多个线程并发访问共享数据资源,且其中至少一个线程执行写操作时,若相关操作未实施适当的同步机制,则程序的最终执行结果将取决于线程调度的具体顺序,从而可能引发不可预期的逻辑错误或数据不一致问题。这本质上是并发编程中需要解决的核心问题之一。
示例(非原子操作的竞态)
#include <iostream>
#include <thread>
// 共享变量
int count = 0;
void increment(){
for (int i = 0; i < 100000; ++i)
{
// 非原子操作:读→加1→写,三步可被中断
// 此处的汇编可以参考下文的图片。
count++;
}
}
int main(){
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
// 结果大概率 < 200000
std::cout << count << std::endl;
return 0;
}
说明:count++ 可以拆解为读、加1、写三个可被中断的步骤,当多个线程交错执行时,就会导致数据错误。

原子操作
原子操作即其执行过程不可被中断,要么完全执行,要么完全不执行,没有中间状态。
C++原子操作通过 std::atomic 模板实现,底层依赖CPU的特定指令(如 lock xadd、cmpxchg),无需程序员显式加锁。
基本用法:std::atomic
std::atomic 是模板类,支持绝大多数基本数据类型(bool、char、int、long、指针等),C++20起支持自定义平凡类型。
核心接口(以 std::atomic<int> 为例)

示例(修复竞态条件)
#include <atomic>
#include <iostream>
#include <thread>
// 共享变量
std::atomic<int> count(0);
void increment(){
for (int i = 0; i < 100000; ++i)
{
// 原子操作
count++;
}
}
int main(){
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
// 结果稳定为 200000
std::cout << count << std::endl;
return 0;
}
说明:上述代码对应的汇编使用“lock xadd”指令实现原子加,其锁定的范围比传统互斥锁更小,因此效率更高。

内存序问题
原子操作的核心目标是保证多线程下共享变量“读-写”操作的原子性,解决竞态条件;而多核CPU架构下的缓存一致性协议(如MESI)是实现的硬件基础。编译器和处理器的指令重排序则会引发线程间内存可见性问题。通过标准化的内存序(std::memory_order)机制,可以精准约束原子操作的内存可见性与执行顺序,在确保正确性的前提下最大化性能。
内存序相关概念说明

核心内存序枚举值(约束强度从高到低)

示例(生产者-消费者)
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> ready(false);
std::atomic<int> data(0);
// 生产者线程:写数据,释放内存序
void Producer(){
// 步骤1:松散序写入data,仅保证原子性
data.store(42, std::memory_order_relaxed);
// 步骤2:释放序写入ready
// 保证本线程中,步骤1的写入对后续用acquire读取此ready的线程可见
ready.store(true, std::memory_order_release);
}
// 消费者线程:读数据,获取内存序
void Consumer(){
// 步骤1:循环用acquire序读取ready
// 一旦读到true,保证能看到生产者release之前的所有写入
while (!ready.load(std::memory_order_acquire))
{
// 自旋等待
}
// 步骤2:松散序读取data,此时必然能看到data=42
std::cout << data.load(std::memory_order_relaxed) << std::endl;
}
int main(){
std::thread t1(Producer);
std::thread t2(Consumer);
t1.join();
t2.join();
return 0;
}
关键细节说明:
memory_order_acquire(获取序)与生产者的 release 形成配对,建立了同步关系,保证了data写入的可见性,并禁止了data.load被重排到ready.load之前。
- 循环空等(自旋)在此仅为简化示例,实际应用中可考虑使用
std::condition_variable 以减少CPU占用。
比较并交换(CAS):无锁编程核心
CAS是原子操作中的关键工具,用于实现无锁的条件更新。其逻辑是:若原子变量的当前值等于预期值(expected),则将其原子性地替换为目标值(desired)并返回 true;否则不修改,返回 false。
接口简介
compare_exchange_weak(expected, desired, mem_order):弱版本,可能发生“伪失败”(值匹配但返回false),通常需在循环中调用,性能更高。
compare_exchange_strong(expected, desired, mem_order):强版本,无伪失败,但性能略低。
compare_exchange_weak的返回值规则

- 返回
true:CAS成功,原子变量已更新为desired。
- 返回
false:可能是真实失败(值不相等),也可能是伪失败(值相等)。失败时,expected 通常会被更新为原子变量的实际当前值。
- 实际使用中,
weak版本几乎总要配合循环。
使用示例
std::atomic<int> ai{1}; // 初始值1
int expected = 1;
const int desired = 2;
// 循环使用weak版本,抵消伪失败
while (!ai.compare_exchange_weak(expected, desired)) {
// 失败时,expected已被更新为当前实际值,下次循环用新值重试
}
// 循环退出时,ai的值一定是2
compare_exchange_weak和compare_exchange_strong的返回值对比

无锁编程示例(无锁栈的核心逻辑)
这个例子展示了如何利用CAS实现一个基础的无锁数据结构。
#include <atomic>
#include <iostream>
struct Node{
int value;
Node* next;
};
// 栈顶指针(原子指针)
std::atomic<Node*> head(nullptr);
// 无锁入栈
void push(int val){
Node* newNode = new Node{val, nullptr};
Node* oldHead = head.load(std::memory_order_relaxed);
// 循环CAS:直到替换栈顶成功
do {
// 新节点指向旧栈顶
newNode->next = oldHead;
} while (!head.compare_exchange_weak(
oldHead, newNode,
// 成功时:释放内存序
std::memory_order_release,
// 失败时:松散序
std::memory_order_relaxed));
}
int main(){
push(10);
push(20);
Node* top = head.load();
// 输出 20
std::cout << top->value << std::endl;
return 0;
}
原子类型的限制
- 支持的类型:默认支持标量类型,C++20支持平凡可复制类型。
- 不支持的操作:原子类型不支持重载的复杂运算符(
+=等除外),需通过 fetch_* 系列接口实现。
- 大小与锁:超过系统指针大小的类型可能由库内部加锁实现,可通过
std::atomic<T>::is_lock_free() 成员函数查询是否真正无锁。
原子操作与互斥锁对比

技术选型原则:
- 简单共享变量(计数器、标志位)优先使用原子操作。
- 多步操作或复杂的临界区(需修改多个关联变量)应使用互斥锁。
- 高性能无锁设计,可基于CAS组合实现原子操作。
高级用法:原子标志与内存屏障
最轻量的原子类型:std::atomic_flag
仅支持 test_and_set()(置1并返回旧值)和 clear()(置0),是唯一保证在所有平台上都无锁的原子类型。
示例(简易自旋锁)
#include <atomic>
#include <iostream>
#include <thread>
// 初始化必须用该宏
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void CriticalSection(int id){
// 自旋等待,直到获取锁(test_and_set 返回 false)
while (lock.test_and_set(std::memory_order_acquire))
{
}
std::cout << "Thread " << id << " enter critical section" << std::endl;
// ... 临界区操作 ...
// 释放锁
lock.clear(std::memory_order_release);
}
int main(){
std::thread t1(CriticalSection, 1);
std::thread t2(CriticalSection, 2);
t1.join();
t2.join();
return 0;
}
内存屏障:std::atomic_thread_fence
内存屏障是用于约束编译器及CPU对内存操作重排序、并保证可见性的底层指令。std::atomic_thread_fence 提供了一种独立于原子变量的显式屏障控制。
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> flag(false);
int data = 0;
void Producer(){
data = 42;
// 释放屏障:保证此前的写操作对后续获取屏障可见
std::atomic_thread_fence(std::memory_order_release);
flag.store(true, std::memory_order_relaxed);
}
void Consumer(){
while (!flag.load(std::memory_order_relaxed))
{
}
// 获取屏障:保证能看到释放屏障之前的所有写操作
std::atomic_thread_fence(std::memory_order_acquire);
// 稳定输出 42
std::cout << data << std::endl;
}
int main(){
std::thread t1(Producer);
std::thread t2(Consumer);
t1.join();
t2.join();
return 0;
}
常见坑与避坑指南
-
误用原子操作(非原子的复合操作)
std::atomic<int> a(0);
// 错误:非原子!拆解为 load(a) → +1 → store(a)
a = a + 1;
// 正确做法
a.fetch_add(1); // 或 a++;
-
内存序过度使用 seq_cst
memory_order_seq_cst 是默认值,但性能开销最大。仅在需要严格的全局顺序一致性时使用,其他场景应优先考虑 acquire/release 或 relaxed。
-
CAS伪失败未处理
compare_exchange_weak 可能伪失败,必须循环调用。
int expected = a.load();
// 错误:无循环,可能因伪失败而失败
a.compare_exchange_weak(expected, 10);
// 正确:循环直到成功
while (!a.compare_exchange_weak(expected, 10)) {}
-
原子类型的拷贝/赋值限制
std::atomic 对象不可拷贝或移动。
std::atomic<int> a(5);
// 编译错误:拷贝构造被删除
// std::atomic<int> b = a;
// 正确做法:通过 load/store 传递值
std::atomic<int> b;
b.store(a.load());
总结
- 核心价值:原子操作是构建高效无锁并发的基础,相比互斥锁开销更低,适用于简单共享变量的同步。
- 关键要点:
- 优先使用
std::atomic 替代“裸变量+锁”。
- 根据场景合理选择内存序,避免滥用
memory_order_seq_cst。
- 使用CAS(特别是
weak版本)时,务必循环处理伪失败。
- 适用场景:计数器、标志位、无锁数据结构(栈/队列)、线程间简单同步。
- 边界注意:复杂临界区仍需互斥锁;对于自定义或大类型,使用前可通过
is_lock_free() 确认其是否真为无锁实现,这对于理解多线程程序性能至关重要。