原子操作(Atomic Operations)是现代多核编程中不可或缺的利器。简单来说,原子操作就是在多线程环境下不会被线程调度机制打断的操作——要么完全执行,要么完全不执行,不存在中间状态。这种特性使得原子操作成为构建线程安全数据结构的基石。
在硬件层面,CPU通过特殊指令实现原子性。以x86架构为例:
这些硬件特性被C++标准库抽象为统一的atomic模板类。例如,当我们使用atomic<int>时,编译器会根据目标平台选择最优的指令实现。在x86上可能是带LOCK前缀的ADD指令,而在ARM架构上则会编译为LL/SC(Load-Linked/Store-Conditional)指令序列。
关键提示:原子操作不是免费的午餐。x86架构的原子读操作与普通读开销相当,但原子写操作可能比普通写慢10倍以上,因为需要保证缓存一致性。
C++提供了6种内存顺序(memory_order),理解它们对编写高性能并发代码至关重要:
| 内存顺序 | 特性 | 典型使用场景 |
|---|---|---|
| memory_order_relaxed | 只保证原子性,不保证顺序 | 计数器、统计量 |
| memory_order_consume | 数据依赖顺序 | 很少使用 |
| memory_order_acquire | 本线程后续读操作必须在本操作之后 | 锁获取 |
| memory_order_release | 本线程前述写操作必须在本操作之前 | 锁释放 |
| memory_order_acq_rel | acquire+release组合 | 读-修改-写操作 |
| memory_order_seq_cst | 全局顺序一致性(默认) | 需要严格同步的场景 |
实际开发中最常见的组合是:
cpp复制// 生产者-消费者模式中的典型用法
std::atomic<bool> ready{false};
int data = 0;
// 生产者线程
data = 42;
ready.store(true, std::memory_order_release);
// 消费者线程
while(!ready.load(std::memory_order_acquire));
assert(data == 42); // 保证能看到data=42
这种acquire-release配对比完全的seq_cst有更好的性能,同时能保证正确的同步语义。
无锁队列是原子操作的经典应用。以下是一个简单的单生产者单消费者队列实现片段:
cpp复制template<typename T>
class LockFreeQueue {
struct Node {
T data;
std::atomic<Node*> next;
Node(const T& data) : data(data), next(nullptr) {}
};
std::atomic<Node*> head;
std::atomic<Node*> tail;
public:
void push(const T& data) {
Node* newNode = new Node(data);
Node* oldTail = tail.load(std::memory_order_relaxed);
while(!tail.compare_exchange_weak(oldTail, newNode,
std::memory_order_release, std::memory_order_relaxed));
oldTail->next.store(newNode, std::memory_order_release);
}
bool pop(T& result) {
Node* oldHead = head.load(std::memory_order_relaxed);
Node* next = oldHead->next.load(std::memory_order_acquire);
if(next == nullptr) return false;
result = next->data;
head.store(next, std::memory_order_release);
delete oldHead;
return true;
}
};
智能指针的线程安全实现依赖原子操作:
cpp复制template<typename T>
class SharedPtr {
T* ptr;
std::atomic<int>* count;
public:
~SharedPtr() {
if(count && --*count == 0) {
delete ptr;
delete count;
}
}
// 其他成员函数...
};
当多个原子变量位于同一缓存行(通常64字节)时,会导致严重的性能下降。解决方案:
cpp复制// 方法1:手动填充
struct AlignedAtomic {
std::atomic<int> value;
char padding[64 - sizeof(std::atomic<int>)];
};
// 方法2:C++17起可用alignas
struct alignas(64) AlignedAtomic {
std::atomic<int> value;
};
ABA问题是指一个值从A变成B又变回A,导致CAS操作误判。解决方案是使用带版本的原子类型:
cpp复制struct VersionedPtr {
void* ptr;
uintptr_t version;
};
std::atomic<VersionedPtr> atomicPtr;
// 更新时比较指针和版本号
VersionedPtr old = atomicPtr.load();
VersionedPtr new{newPtr, old.version + 1};
while(!atomicPtr.compare_exchange_weak(old, new));
下表对比了不同同步机制的性能(纳秒/操作,测试平台:Intel i7-11800H):
| 操作类型 | 单线程 | 多线程竞争 |
|---|---|---|
| 普通变量读写 | 1-3 | 数据竞争未定义 |
| atomic relaxed | 5-10 | 20-50 |
| atomic seq_cst | 10-20 | 50-200 |
| mutex锁 | 20-30 | 1000-5000 |
| 自旋锁 | 15-25 | 500-2000 |
C++20引入了若干原子操作增强:
atomic_ref:允许对现有变量进行原子操作atomic<shared_ptr>:线程安全的共享指针wait/notify:原子变量的条件等待例如,使用atomic_ref:
cpp复制int regularInt = 0;
std::atomic_ref<int> atomicInt(regularInt);
atomicInt.store(42); // 现在regularInt也被原子地修改
在实际工程中,我倾向于遵循以下最佳实践:
调试原子操作时,TSAN(Thread Sanitizer)是不可或缺的工具。在GCC/Clang中通过-fsanitize=thread启用,可以检测数据竞争和内存顺序问题。