关于C++ memory_order_seq_cst的内存序保证与死锁问题问询
场景代码
#include <atomic> #include <thread> #include <cstdint> void race_test() { std::atomic<std::size_t> data = 0; std::atomic_bool flag = false; constexpr std::size_t REP_COUNT = 1000; std::thread inc_thread{[&] { for (std::size_t i = 0; i != REP_COUNT; ++i) { data.fetch_add(1, std::memory_order_relaxed); flag.store(true, std::memory_order_release); flag.notify_one(); } }}; std::thread dec_thread{[&] { for (std::size_t i = 0; i != REP_COUNT; ++i) { while (data.load(std::memory_order_relaxed) == 0) { flag.wait(false, std::memory_order_acquire); flag.store(false, std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_seq_cst); } data.fetch_sub(1, std::memory_order_relaxed); } }}; inc_thread.join(); dec_thread.join(); } int main() { for (std::size_t i = 0; i != 1000; ++i) race_test(); }
核心问题
理想流程是生产者(inc_thread)递增data后设置flag并通知消费者(dec_thread);消费者在data为0时等待flag,清空flag后处理data。当前代码加seq_cst栅栏可运行,但存在以下疑问:
- 为何
memory_order_acq_rel栅栏无法替代seq_cst? - 移除栅栏但将
flag.store(false)改为memory_order_seq_cst是否合规? seq_cst是真的保证无死锁,还是依赖x86平台特性?- 如何修改代码确保符合标准且无死锁?
问题分析与解答
死锁根源
移除栅栏后偶发死锁,本质是消费者的flag.store(false)可能在生产者的flag.notify_one()之前执行,导致通知被覆盖:生产者设置flag=true后,消费者抢先将flag重置为false,后续生产者的notify_one()无法唤醒已进入等待的消费者,最终消费者因生产者完成所有循环而永久等待。
1. 为何acq_rel栅栏无法替代seq_cst?
memory_order_acq_rel仅保证当前线程内的内存操作顺序(栅栏前的存储不后移,栅栏后的加载不前移),但不强制全局总序。在弱内存模型平台(如ARM、PowerPC),不同线程可能看到操作顺序不一致:消费者的flag.store(false)可能被其他线程视为早于生产者的notify_one()执行,导致通知丢失。
而memory_order_seq_cst会建立全局统一的操作总序,所有线程看到的seq_cst操作顺序完全一致,确保生产者的flag.store(true)→notify_one()的顺序不会被消费者的flag.store(false)打乱,从根源避免通知覆盖。
2. 将flag.store(false)改为seq_cst是否合规?
完全合规。seq_cst是release的超集,既保证当前线程内的顺序约束,又参与全局总序。将flag.store(false)设为seq_cst后,该操作不会被重排到之前的data.load()之前,同时与生产者的flag.store(true, release)形成可靠同步,有效避免死锁。
3. seq_cst是否依赖x86平台特性?
seq_cst是C标准明确规定的内存顺序,不依赖x86的强内存模型。x86默认禁止大部分内存重排,使得非seq_cst操作也能表现出类似seq_cst的效果,但在弱内存模型平台(如ARM),若无seq_cst约束,内存重排会直接导致死锁。seq_cst的全局总序保证在任何符合C标准的平台上都能生效。
4. 符合标准的无死锁修改方案
方案一:优化flag同步逻辑
保留flag机制,用seq_cst存储替代栅栏,简化代码且保证跨平台可靠性:
#include <atomic> #include <thread> #include <cstdint> void race_test() { std::atomic<std::size_t> data = 0; std::atomic_bool flag = false; constexpr std::size_t REP_COUNT = 1000; std::thread inc_thread{[&] { for (std::size_t i = 0; i != REP_COUNT; ++i) { data.fetch_add(1, std::memory_order_relaxed); flag.store(true, std::memory_order_seq_cst); flag.notify_one(); } }}; std::thread dec_thread{[&] { for (std::size_t i = 0; i != REP_COUNT; ++i) { while (data.load(std::memory_order_relaxed) == 0) { flag.wait(false, std::memory_order_acquire); flag.store(false, std::memory_order_seq_cst); } data.fetch_sub(1, std::memory_order_relaxed); } }}; inc_thread.join(); dec_thread.join(); } int main() { for (std::size_t i = 0; i != 1000; ++i) race_test(); }
方案二:移除flag,直接用data作为通知对象
更简洁的方案是利用data自身的变化作为通知条件,彻底避免flag覆盖问题:
#include <atomic> #include <thread> #include <cstdint> void race_test() { std::atomic<std::size_t> data = 0; constexpr std::size_t REP_COUNT = 1000; std::thread inc_thread{[&] { for (std::size_t i = 0; i != REP_COUNT; ++i) { auto old_val = data.fetch_add(1, std::memory_order_release); if (old_val == 0) { // 仅当data从0变为1时通知,避免无效重复通知 data.notify_one(); } } }}; std::thread dec_thread{[&] { for (std::size_t i = 0; i != REP_COUNT; ++i) { while (data.load(std::memory_order_acquire) == 0) { data.wait(0, std::memory_order_acquire); } data.fetch_sub(1, std::memory_order_release); } }}; inc_thread.join(); dec_thread.join(); } int main() { for (std::size_t i = 0; i != 1000; ++i) race_test(); }
该方案通过data的release-acquire同步保证数据可见性,仅在必要时发送通知,完全符合C++标准且无死锁风险。
内容的提问来源于stack exchange,提问作者Jordan Woyak

