参考:
- C++ atomic 原子操作_c++ 原子操作-CSDN博客
- DeepSeek
1. 什么是原子操作
原子操作(Atomic Operations)是不可分割的操作,保证在多线程环境中执行时不会被中断,避免数据竞争(Data Race)。即:原子操作要么完全执行,要么完全不执行,不会在执行期间被其他线程操作干扰。
C++11引入<atomic>
头文件,提供std::atomic<T>
模板类,支持对基本类型(如int
、bool
、指针)的原子操作。
2. 原子操作的特点
- 无锁(Lock-Free): 原子操作通常由硬件指令(如CAS,LL/SC)实现,无需显式加锁。
- 线程安全: 保证对变量的读写操作在多线程环境下的正确性。
- 轻量级: 适合简单操作(如计数器、标志位),性能通常高于互斥锁。
示例:
#include <atomic>
std::atomic<int> counter(0);
void increment() {
counter.fetch_add(1, std::memory_order_relaxed); // 原子递增
}
3. 原子操作的底层原理
原子操作的实现依赖于硬件指令,例如:
CAS(Compare-And-Swap):通过比较内存中的值与预期值,若匹配则更新为新值。
bool compare_exchange_strong(T& expected, T desired) { if (current == expected) { current = desired; return true; } else { expected = current; return false; } }
LL/SC(Load-Linked/Store-Conditional):在特定地址上标记“监视”,若未被其他线程修改则写入。
这些指令保证操作的原子性,但不同硬件(x86、ARM)的实现差异较大。例如:
- x86:通过
LOCK
前缀指令(如LOCK XCHG
)实现原子操作。 - ARM:依赖LL/SC指令实现无锁原子操作。
原子操作的局限性
- ABA问题:线程A读取值
A
,线程B将值改为B
后又改回A
,导致A的CAS误判无变化。
解决方案:使用带版本号的原子变量(如std::atomic<std::pair<T, uint64_t>>
)。 - 适用范围:仅适用于基本类型(如
int
、bool
、指针)。复杂类型需使用std::atomic_flag
或互斥锁。
4. 内存序
内存序(Memory Order)定义了原子操作之间的可见性和顺序约束,确保多线程环境下的内存访问符合预期。以下问题可能导致指令顺序变化:
- 编译器重排:编译器优化可能导致指令顺序变化。
- CPU重排:现代CPU的乱序执行机制(如Store Buffer、Invalidate Queue)可能打乱指令顺序。
- 缓存一致性:不同核心的缓存状态可能导致内存视图不一致。
C++提供6种内存序,分为三类:
内存序 | 描述 |
---|---|
顺序一致性(Sequential Consistency) | |
memory_order_seq_cst |
最严格,保证全局顺序一致,性能较低。默认选项。 |
获取-释放(Acquire-Release) | |
memory_order_acquire |
保证后续读操作不会重排到该操作之前(用于“获取”同步)。 |
memory_order_release |
保证前面的写操作不会重排到该操作之后(用于“释放”同步)。 |
memory_order_acq_rel |
同时包含acquire和release语义(用于读-改-写操作)。 |
宽松(Relaxed) | |
memory_order_relaxed |
仅保证原子性,无顺序约束(适用于计数器等无需同步的场景)。 |
memory_order_consume |
依赖顺序(较弱的acquire,C++17后不推荐使用)。 |
典型场景:
生产者-消费者模型:
// 生产者线程
data = ...; // 生产数据
flag.store(true, std::memory_order_release); // 发布数据
// 消费者线程
while (!flag.load(std::memory_order_acquire)); // 获取数据
use_data(data); // 安全使用数据
内存屏障
1. 内存屏障的作用
内存屏障是硬件或编译器级别的指令,用于强制限制内存操作的执行顺序,分为两类:
- 编译器屏障:阻止编译器重排指令(如
asm volatile("" ::: "memory")
)。 - 硬件屏障:阻止CPU重排内存访问(如x86的
MFENCE
、ARM的DMB
)。
2. C++内存序与内存屏障的映射
std::atomic_thread_fence()
:显式插入内存屏障。std::atomic_thread_fence(std::memory_order_acquire); // 插入读屏障
隐式屏障:原子操作的内存序参数隐式插入屏障:
a.store(1, std::memory_order_release); // 隐式插入Store屏障
3. 内存屏障的实际案例
示例:无锁队列的入队操作
// 生产者线程
Node* new_node = new Node(data);
new_node->next.store(head, std::memory_order_relaxed);
head.store(new_node, std::memory_order_release); // 插入写屏障,确保new_node初始化完成后再更新head
// 消费者线程
Node* local_head = head.load(std::memory_order_acquire); // 插入读屏障,确保读取到最新的head
if (local_head != nullptr) {
// 安全操作local_head->next
}
5. 原子操作和互斥锁的对比
特性 | 原子操作 | 互斥锁(如std::mutex) |
---|---|---|
实现方式 | 硬件指令(如CAS)实现无锁操作。 | 通过操作系统内核的锁机制(可能涉及上下文切换)。 |
性能 | 低竞争时性能高,高竞争时可能自旋。 | 高竞争时可能更高效(线程休眠)。 |
适用场景 | 简单操作(如计数器、标志位)。 | 复杂操作或需要保护多个变量/代码块。 |
内存序复杂性 | 需显式指定内存序,易出错。 | 隐式保证顺序一致性,更简单。 |
死锁风险 | 无。 | 需避免死锁(如加锁顺序不一致)。 |
ABA问题 | 可能发生(需配合版本号解决)。 | 无。 |
选择建议:
- 使用原子操作:需要高性能的简单操作,且能正确处理内存序。
- 使用互斥锁:保护复杂逻辑或临界区较长时,简化代码并减少错误。
6. 常用的原子操作
load
:读取原子变量的值std::atomic<int> a(1); int value = a.load();
store
:将一个值存储到原子变量中std::atomic<int> a(1); a.store(2); // a的值变为2
exchange
:将原子变量的值替换为另外一个值,并返回旧值常用来在并发环境下进行“交换”操作。
std::atomic<int> a(1); int old_value = a.exchange(2); // old_value为1,a的值变为2
compare_exchange_weak / compare_exchange_strong
:原子地进行条件交换操作。若当前值等于预期值,则交换新值,否则返回false
。weak
失败后可能会重新尝试,性能相对较高;strong
失败后会返回false
并不再尝试。std::atomic<int> a(1); int expected = 1; if (a.compare_exchange_weak(expected, 2)) { // 如果a的值是1,设置为10 std::cout << "Value changed!" << std::endl; } else { std::cout << "Value not changed!" << std::endl; }
fetch_add / fetch_sub
:原子地执行加法或减法操作,并返回旧值。std::atomic<int> a(5); int old_value = a.fetch_add(1); // old_value为5,a为6
7. 相关问题讨论
- 使用
std::atomic
实现线程安全计数器:C++ atomic 原子操作_c++ 原子操作-CSDN博客 - 使用
compare_exchange_strong
实现自旋锁:C++ atomic 原子操作_c++ 原子操作-CSDN博客 - 原子操作支持的运算和不支持的运算:C++ atomic 原子操作_c++ 原子操作-CSDN博客
- 什么是ABA问题,如何解决:ABA问题及其解决思路C+±CSDN博客