原子操作是指不可被中断的单个或一组操作
在多线程环境下,原子操作的执行过程中不会被其他线程打断,要么完全执行完毕,要么完全不执行,不存在 “执行一半” 的中间状态。
它的核心价值是解决多线程对共享数据的竞态条件(Race Condition) 问题(比如两个线程同时读写同一个变量,导致数据错乱),是实现线程安全的基础,无需依赖互斥锁(如std::mutex)即可保证简单数据操作的安全性。
非原子操作的反例(多线程修改同一个变量):
#include <iostream>
#include <thread>
using namespace std;int Counter = 0;void Increment() {for (int i = 0; i < 100000; ++i) {Counter++; // 非原子操作!}
}int main() {thread t1(Increment);thread t2(Increment);t1.join();t2.join();cout << "Counter = " << Counter << endl; // 结果大概率≠200000return 0;
}
输出结果:Counter = 105516 每次不一样
原因分析:
Counter++ 看似是 “一步操作”,实际拆解为 3 个步骤:
1.读取Counter的当前值到寄存器;
2.寄存器值 + 1;
3.把新值写回Counter。
多线程执行时,可能出现 “线程 A 读值后,线程 B 抢先修改并写回,线程 A 再写回旧值 + 1” 的情况,导致计数丢失。
C++11 引入
而原子操作能让Counter++变成 “不可拆分” 的一步,避免上述问题。
加入原子操作后:
#include <iostream>
#include <thread>
#include <atomic> // 原子操作头文件
using namespace std;atomic<int> Counter = 0; // 原子整型void Increment() {for (int i = 0; i < 100000; ++i) {Counter++; // 原子自增,等价于 Counter.fetch_add(1)}
}int main() {thread t1(Increment);thread t2(Increment);t1.join();t2.join();cout << "Counter = " << Counter << endl; // 结果必然=200000return 0;
}
输也结果:Counter = 200000
核心操作(以std::atomic
a.load() 原子读取值(默认内存序memory_order_seq_cst) 等价原生指令(x86): mov
a.store(val) 原子写入值 等价原生指令(x86): mov
a++/a-- 原子自增 / 自减(语法糖) 等价原生指令(x86): lock xadd
a.fetch_add(n) 原子加 n,返回旧值 等价原生指令(x86): lock xadd
a.fetch_sub(n) 原子减 n,返回旧值 等价原生指令(x86): lock xadd
a.exchange(val) 原子替换值,返回旧值 等价原生指令(x86): xchg
a.compare_exchange_weak(expected, desired) 比较并交换(CAS):若a==expected,则设为desired,返回 true;否则更新expected为a的当前值,返回 false 等价原生指令(x86): lock cmpxchg
原子操作的底层机制分析上面的代码:
无原子操作时:
Counter++会拆分为 3 步:
mov eax, [Counter](读值到寄存器);
add eax, 1(寄存器 + 1);
mov [Counter], eax(写回内存)。
此时线程协调会出问题,比如:
| 时间片 | Core0(t1) | Core1(t2) | Counter 值 | 问题 |
|---|---|---|---|---|
| 1 | 读 Counter=0 到 eax | - | 0 | - |
| 2 | - | 读 Counter=0 到 eax | 0 | 两个线程都读到旧值 0 |
| 3 | eax+1=1 | eax+1=1 | 0 | 都计算出 1 |
| 4 | 写回 Counter=1 | - | 1 | t1 先写回 |
| 5 | - | 写回 Counter=1 | 1 | t2 覆盖 t1 的结果,丢失一次自增 |
上面 两次读写同样的数,覆盖了,造成数据缺失
而原子操作通过lock xadd把 “读 - 改 - 写” 打包成不可拆分的指令,从硬件层面杜绝了 “线程 A 读值后,线程 B 抢先修改” 的情况
代码中Counter是std::atomic
原子操作的底层实现(x86 平台为例)
Counter++(原子自增)对应的 CPU 指令是:lock xadd dword ptr [Counter], 1
xadd:是 “交换并相加” 指令,完成「读取值→加 1→写回值→返回旧值」的完整逻辑;
lock前缀:是 CPU 的 “总线锁定 / 缓存锁定” 机制 ——在执行xadd期间,锁定 Counter 对应的内存地址,禁止其他 CPU 核心(线程)访问该地址,直到指令执行完毕。
这是原子操作能 “协调” 多线程的硬件基础:任何时刻,只有一个线程能执行对 Counter 的原子操作,其他线程必须等待。
| 时间片 | Core0(t1) | Core1(t2) | Counter 值 | 关键说明 |
|---|---|---|---|---|
| 1 | 执行Counter++(lock xadd) | 等待(总线被 Core0 锁定) | 0→1 | t1 的原子操作独占内存,t2 无法打断 |
| 2 | 释放总线锁定 | 执行Counter++(lock xadd) | 1→2 | t2 抢占到总线,执行原子操作 |
| 3 | 执行Counter++(lock xadd) | 等待(总线被 Core0 锁定) | 2→3 | 交替执行,无冲突 |
| ... | 循环执行剩余 99997 次自增 | 循环执行剩余 99998 次自增 | ... | 每次自增都是原子的,无中间状态 |
| 最终 | 执行完 10 万次 | 执行完 10 万次 | 200000 | 所有操作无丢失,结果准确 |
关键:内存序(Memory Order)
原子操作的 “内存可见性” 可通过内存序优化(默认是最严格的memory_order_seq_cst,性能稍低),常见内存序:
memory_order_relaxed:仅保证操作本身原子性,不保证内存可见性(最快,适用于无依赖的计数);
memory_order_acquire:读操作,保证后续操作能看到当前操作的结果;
memory_order_release:写操作,保证当前操作的结果对后续读操作可见;
memory_order_seq_cst:顺序一致性(默认),所有线程看到的操作顺序一致(最安全,性能稍差)。
示例( Relaxed 内存序优化计数):
atomic<int> Counter = 0;
void Increment() {for (int i = 0; i < 100000; ++i) {Counter.fetch_add(1, memory_order_relaxed); // 仅需原子性,无需严格内存序}
}
UE5 中的原子操作(与标准 C++ 的关联)
FPlatformAtomics::InterlockedOr/InterlockedAnd等函数,本质是 UE 对平台相关原子操作的封装(兼容 Windows/Linux/PS5 等平台),对应标准 C++ 的原子操作:
FPlatformAtomics::InterlockedOr → 原子按位或(等价于atomic
FPlatformAtomics::InterlockedAnd → 原子按位与(等价于atomic
FPlatformAtomics::AtomicRead_Relaxed → 放松内存序的原子读(等价于atomic
比如 UE5 中UObjectBase的原子标志位操作:
// UE5原子设置标志位(核心逻辑)
FPlatformAtomics::InterlockedOr((int32*)&ObjectFlags, FlagsToAdd);
// 等价于标准C++
atomic<int32> objFlags;
objFlags.fetch_or(FlagsToAdd);
原子操作 vs 互斥锁
| 特性 | 原子操作 | 互斥锁(std::mutex) |
|---|---|---|
| 适用场景 | 简单数据操作(计数、标志位、CAS) | 复杂操作(多步逻辑、跨资源访问) |
| 性能 | 极快(无内核态切换) | 较慢(可能触发内核态切换) |
| 功能 | 仅支持基础数据操作 | 支持任意临界区保护 |
| 死锁风险 | 无 | 有(如锁顺序错误) |
核心总结
原子操作是 “不可中断” 的操作,解决多线程共享数据的竞态条件;
C++ 通过
优先用原子操作处理简单数据(计数、标志位),复杂逻辑用互斥锁;
内存序可优化性能,按需选择(非特殊场景用默认seq_cst即可)。