使用relaxed内存序能否保证flag.clear()先于互斥锁获取执行?
std::atomic_flag flag; // 类的成员变量 std::mutex m; flag.test_and_set(); // 在f()或f2()执行前设为true // 仅执行一次的“停止”函数 void f() { flag.clear(std::memory_order_relaxed); { std::unique_lock<std::mutex> lock(m); // 执行一些操作 } // 等待f2线程结束 } // 长期运行的函数 void f2() { std::unique_lock<std::mutex> lock(m); while (flag.test(std::memory_order_relaxed)) { // 执行循环操作 } }
问题
通常f2()先启动,f()用于终止f2()。我们需要保证flag.clear()在获取互斥锁之前执行,否则f()无法及时返回。
请问:使用relaxed内存序是否足以保证flag.clear()先于互斥锁获取操作执行?
另外延伸几个相关疑问:
- 类似场景中
std::thread::join作为同步操作能保证flag.clear()不会被重排到join之后,但这里用的是std::mutex,如果换成仅用acquire内存序的自旋锁,relaxed序是否依然安全? acquire仅阻止后续操作重排到它之前,不限制存储操作重排到acquire之后,那这里是否需要StoreLoad屏障或全内存屏障?- 是不是必须用
seq_cst内存序才能阻止互斥锁获取操作重排到flag.clear()之前?
回答
核心结论:relaxed内存序不足以保证flag.clear()先于互斥锁获取操作执行
1. 关于std::mutex的情况
std::mutex的锁获取操作(lock())是acquire语义,锁释放是release语义,但这里的关键是:CPU和编译器完全可能把flag.clear()(relaxed存储)重排到lock(m)之后。因为relaxed存储没有任何内存屏障约束,而acquire操作只禁止之后的读写操作被重排到acquire之前,不限制之前的存储操作被重排到acquire之后。
这就会导致:f()先去抢锁,抢到之后才执行flag.clear(),但此时f2()可能还持有锁在循环,f()会被阻塞在锁上,无法及时终止f2(),完全违背了设计意图。
2. 换成acquire自旋锁的情况
如果自旋锁的lock()只有acquire语义,情况和std::mutex完全一样——依然无法阻止flag.clear()被重排到锁获取之后。因为acquire语义不约束前面的存储操作向后重排,所以relaxed的clear()还是可能被放到锁获取之后执行,问题依然存在。
3. 需要什么内存屏障或内存序?
要阻止flag.clear()被重排到锁获取之后,我们需要在clear()和lock()之间插入StoreLoad屏障,或者直接给flag.clear()使用release语义(std::memory_order_release)。
- 用release语义的话,release操作会禁止当前线程中后续的读写操作被重排到release之前,正好能保证
flag.clear()一定在lock(m)之前执行。 - 如果用relaxed+显式屏障,比如在
clear()后插入std::atomic_thread_fence(std::memory_order_release),效果和直接用release语义的clear()一致。
至于seq_cst,它确实能解决问题,但属于“杀鸡用牛刀”——seq_cst会带来全序约束,性能开销比release大很多,这里完全没必要用,release语义就足够满足需求。
补充:为什么join能保证?
std::thread::join()是一个同步操作,它会强制线程中所有之前的内存操作完成后才会执行join之后的代码,同时也禁止之前的操作被重排到join之后。但mutex的lock操作没有这个约束,它只约束自身之后的操作,不约束之前的存储向后重排。
内容的提问来源于stack exchange,提问作者j0011

