我第一次接触原子操作是在一个高频交易系统的性能优化项目中。当时系统里的锁竞争导致吞吐量始终上不去,直到团队里的老架构师扔给我一份std::atomic的文档。原子操作就像超市收银台的"一件商品扫码"动作——要么完整扫完条形码,要么完全没扫,绝不会出现扫到一半被其他顾客打断的情况。
在硬件层面,现代CPU通过总线锁和缓存一致性协议实现原子性。比如x86架构的LOCK指令前缀会锁定内存总线,而ARM架构则采用LL/SC(Load-Link/Store-Conditional)机制。这就像多个收银员共用一台扫码枪时,系统会确保同一时间只有一人能握住枪柄。
看这个典型问题场景:
cpp复制int shared = 0; // 普通int变量
void increment() {
for(int i=0; i<10000; ++i) {
++shared; // 非原子操作
}
}
当两个线程同时执行increment()时,最终shared的值可能远小于20000。我在测试中遇到过最夸张的一次结果只有13245,因为编译器生成的汇编代码实际上包含:
code复制mov eax, [shared] ; 读取内存到寄存器
inc eax ; 寄存器加1
mov [shared], eax ; 写回内存
std::atomic真正的魔法在于内存序(memory_order)参数。记得第一次看到这段代码时我完全懵了:
cpp复制std::atomic<int> counter(0);
counter.fetch_add(1, std::memory_order_release);
内存序就像快递站的包裹分拣规则:
memory_order_relaxed:就像把包裹随便扔进某个筐,只要最终数量对就行memory_order_acquire:保证拿到包裹时,之前的所有包裹都已到位memory_order_release:确保当前包裹放好后,之前的包裹肯定都放好了memory_order_seq_cst(默认):最严格的全局顺序,像给每个包裹贴精确时间戳实测一个典型场景:用atomic实现简单的发布-订阅模型
cpp复制std::atomic<bool> data_ready(false);
int payload = 0;
// 生产者线程
void producer() {
payload = 42; // 1.准备数据
data_ready.store(true, std::memory_order_release); // 2.发布
}
// 消费者线程
void consumer() {
while(!data_ready.load(std::memory_order_acquire)); // 3.等待
assert(payload == 42); // 4.使用数据
}
如果没有正确内存序,断言可能失败,因为编译器和CPU会重排序指令。
Compare-And-Swap(CAS)是无锁编程的核心武器,但它的使用远比想象中复杂。我曾踩过一个坑:
cpp复制std::atomic<int> value(0);
bool update(int new_val) {
int old = value.load();
while(!value.compare_exchange_weak(old, new_val)) {
// 这里可能无限循环!
}
return true;
}
这段代码在ARM架构下可能死循环,因为compare_exchange_weak允许虚假失败。正确的写法应该加入退出条件:
cpp复制int attempts = 0;
while(!value.compare_exchange_weak(old, new_val)) {
if(++attempts > 100) return false;
old = value.load(); // 必须重新加载
}
我实现的第一个无锁栈长这样:
cpp复制template<typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head;
public:
void push(const T& data) {
Node* new_node = new Node{data, nullptr};
new_node->next = head.load();
while(!head.compare_exchange_weak(new_node->next, new_node));
}
};
这个版本存在ABA问题——当线程A读取head后挂起,线程B弹出所有节点又压入相同地址的新节点,线程A的CAS仍会成功。解决方案是使用带标签的指针:
cpp复制struct TaggedPtr {
Node* ptr;
uintptr_t tag;
};
std::atomic<TaggedPtr> head;
每次修改时tag自增,这样即使地址相同也能检测出变化。
Michael-Scott队列是最经典的无锁队列,但实现时容易忽略细节:
cpp复制struct Node {
std::atomic<Node*> next;
T data;
};
std::atomic<Node*> head, tail;
void enqueue(T data) {
Node* new_node = new Node{nullptr, data};
Node* old_tail = tail.load();
while(true) {
Node* next = old_tail->next.load();
if(!next) {
if(old_tail->next.compare_exchange_weak(next, new_node)) {
tail.compare_exchange_weak(old_tail, new_node);
return;
}
} else {
tail.compare_exchange_weak(old_tail, next);
}
old_tail = tail.load();
}
}
这里的关键点在于:
在多核环境下,false sharing是性能杀手。我曾通过一个简单改动将吞吐量提升3倍:
cpp复制struct alignas(64) Counter { // 64字节缓存行对齐
std::atomic<int> value;
};
Counter counters[16];
每个counter独占一个缓存行,避免不同CPU核心间的缓存无效化。
-fsanitize=thread,能检测数据竞争_mm_clflush触发断点std::random_device制造线程切换有次我用TSAN发现一个诡异的竞态条件——两个线程同时修改atomic变量居然报错。最终发现是忘记将指针本身声明为atomic:
cpp复制Node* ptr; // 错误!应该用atomic<Node*>
在电商秒杀系统中,我们对比了三种实现:
但无锁版本在极端情况下会出现10ms的毛刺,因为重试机制导致忙等。最终我们选择混合方案:
记得在实现内存分配器时,简单的原子计数器比无锁链表快5倍,但内存利用率低30%。这种trade-off需要根据具体场景判断。