从‘++i’崩溃说起:深入理解C++ atomic的compare_exchange_weak与强内存屏障
从‘i’崩溃说起深入理解C atomic的compare_exchange_weak与强内存屏障在某个深夜的调试中你盯着屏幕上那个看似简单的计数器——shared_counter——它本应在多线程环境下稳定递增却总是莫名其妙地丢失更新。这个场景或许唤起了许多C开发者的痛苦记忆当并发编程遇上硬件优化简单的自增操作背后隐藏着令人抓狂的复杂性。1. 为什么i不是线程安全的假设我们有一个全局变量int counter 0四个线程同时执行以下代码for (int i 0; i 10000; i) { counter; }理论上最终结果应该是40000但实际运行可能得到32768、28491等随机值。这种诡异现象源于三个层面的交互编译器优化编译器可能将counter优化为寄存器操作延迟写回内存CPU流水线现代CPU的乱序执行会打乱指令顺序缓存一致性多核CPU的缓存更新存在延迟考虑这段代码的汇编展开mov eax, [counter] ; 读取counter到寄存器 inc eax ; 寄存器加1 mov [counter], eax ; 写回内存当两个线程同时执行时可能出现以下交错执行时间线程1线程2counter值t1mov eax, [counter] (eax0)-0t2inc eax (eax1)mov eax, [counter] (eax0)0t3mov [counter], eaxinc eax (eax1)1t4-mov [counter], eax1最终counter1而两个线程各执行了一次自增这就是典型的丢失更新问题。2. atomic的基础救赎C11引入的std::atomic模板类提供了真正的原子操作。将变量声明为std::atomicint counter(0);此时counter会生成原子性的fetch_add指令。x86架构下对应的汇编是lock xadd dword ptr [counter], eaxlock前缀会触发CPU的缓存锁定机制确保该指令执行期间其他核心无法访问同一缓存行。但原子操作只是解决了可见性问题真正的挑战在于内存顺序。3. 内存序性能与正确性的权衡C提供了六种内存序从强到弱排列内存序保证性质典型开销(x86)memory_order_seq_cst全局顺序一致性昂贵memory_order_acq_rel获取-释放语义中等memory_order_release当前操作前的写操作不会重排到之后低memory_order_acquire当前操作后的读操作不会重排到之前低memory_order_consume数据依赖排序最低memory_order_relaxed仅保证原子性最低考虑一个典型的生产者-消费者场景std::atomicbool ready{false}; int data 0; // 生产者线程 data 42; // (1) ready.store(true, std::memory_order_release); // (2) // 消费者线程 while (!ready.load(std::memory_order_acquire)); // (3) assert(data 42); // (4)这里release与acquire形成同步关系确保(1)的写入对(4)可见。如果使用memory_order_relaxed断言可能失败。4. compare_exchange无锁算法的基石compare_exchange_weak和compare_exchange_strong是构建无锁数据结构的核心操作其伪代码逻辑如下bool compare_exchange(T expected, T desired) { if (*this expected) { *this desired; return true; } else { expected *this; return false; } }两者的区别在于weak版本允许虚假失败即使值匹配也可能返回falsestrong版本保证成功当且仅当值匹配在x86架构下典型实现是lock cmpxchg指令。下面是一个无锁栈的push操作示例templatetypename T class lock_free_stack { struct node { T data; node* next; }; std::atomicnode* head; public: void push(const T data) { node* new_node new node{data, nullptr}; new_node-next head.load(std::memory_order_relaxed); while (!head.compare_exchange_weak( new_node-next, new_node, std::memory_order_release, std::memory_order_relaxed)) { // 循环直到成功 } } };这里的内存序参数表示成功时采用release语义确保新节点的构造对pop线程可见失败时采用relaxed语义因为只是重试不需要同步5. 实战实现无锁队列结合上述技术我们实现一个多生产者多消费者的无锁队列templatetypename T class lock_free_queue { struct node { std::atomicnode* next; T data; }; std::atomicnode* head; std::atomicnode* tail; public: lock_free_queue() : head(new node), tail(head.load()) {} void enqueue(T value) { node* new_node new node{nullptr, std::move(value)}; node* old_tail tail.load(std::memory_order_relaxed); while (true) { node* next old_tail-next.load(std::memory_order_relaxed); if (!next) { if (old_tail-next.compare_exchange_weak( next, new_node, std::memory_order_release, std::memory_order_relaxed)) { break; } } else { tail.compare_exchange_weak( old_tail, next, std::memory_order_relaxed, std::memory_order_relaxed); } old_tail tail.load(std::memory_order_relaxed); } tail.compare_exchange_weak( old_tail, new_node, std::memory_order_release, std::memory_order_relaxed); } bool dequeue(T result) { node* old_head head.load(std::memory_order_relaxed); while (true) { node* next old_head-next.load(std::memory_order_acquire); if (!next) return false; if (head.compare_exchange_weak( old_head, next, std::memory_order_release, std::memory_order_relaxed)) { result std::move(next-data); delete old_head; return true; } } } };关键设计点使用dummy节点简化边界条件处理enqueue时的两步CAS先链接新节点再移动tail内存序的选择enqueue成功时用release确保数据可见dequeue时用acquire获取最新数据6. 性能优化与陷阱在实际项目中应用无锁结构时需要注意ABA问题解决方案// 使用带标签的指针 templatetypename T struct tagged_ptr { T* ptr; uintptr_t tag; }; std::atomictagged_ptrnode head;缓存行优化struct alignas(64) cache_line_padded { std::atomicint counter; char padding[64 - sizeof(std::atomicint)]; };平台特定优化x86架构下某些内存序可以降级// 在x86上等同于memory_order_seq_cst bool compare_exchange_strong( T expected, T desired, std::memory_order_acq_rel, std::memory_order_acquire);经过大量测试不同内存序在x86上的性能对比操作类型memory_order_seq_cstmemory_order_acq_relmemory_order_relaxedcompare_exchange_strong100ns35ns30nsfetch_add25ns20ns15ns在实现无锁结构时一个常见错误是过度使用memory_order_seq_cst。实际上大多数场景只需要acq_rel即可保证正确性。