为何memory_order_relaxed不适用于类once_flag的atomic_bool?
原子内存序问题分析:Herb Sutter单例代码的隐患
在Herb Sutter的《atomic<> Weapons》演讲幻灯片第54页,一段单例模式的代码被指出存在内存序问题,以下是原代码:
struct widget { static inline atomic<widget *> instance = nullptr; static inline atomic<bool> create = false; static widget *get_instance() { if (instance.load(memory_order_acquire) == nullptr) { if (!create.exchange(true, memory_order_relaxed)) // <- BAD? instance.store(new widget(), memory_order_release); else while (instance.load(memory_order_acquire) == nullptr) ; } return instance.load(memory_order_acquire); } };
标注为// <- BAD?的行需要修改为:
if (!create.exchange(true, memory_order_acquire)) // <- GOOD?
核心问题
使用BAD行可能会出现什么问题?(即使在虚构的特殊架构上) 或者等价地说,以下认为BAD行没问题的论点存在什么错误?
原论点如下:
- 如果一个或多个线程调用
get_instance(),那么恰好有一个线程会从create.exchange(...)中得到false,因此只会分配一个widget实例,且仅有一个线程会向instance写入数据。 - 由于
instance和create都是原子变量,无论内存序如何,对它们的访问都不会发生数据竞争。 - 由于写入-写入一致性,
get_instance()不可能返回nullptr或除单次new widget()执行结果之外的任何值。 - 由于对
instance的唯一非nullptr写入使用了release语义,且所有对instance的读取都使用acquire语义,所有非nullptr的读取都会与该写入同步,这意味着widget构造函数中的所有操作都会在get_instance()返回后对实例的任何访问之前完成,因此访问widget的非原子内容也不会出现未定义行为。
问题分析
原论点的核心漏洞在于忽略了弱内存模型下指令重排序的可能性,以及relaxed内存序无法提供必要的内存屏障来阻止这种重排序。
具体来说:
- 对于进入分支的等待线程,
create.exchange(true, memory_order_relaxed)是relaxed内存序,这意味着编译器或CPU完全可以将后续的instance.load(memory_order_acquire)操作重排到create.exchange之前执行。 - 假设发生了这种重排序:等待线程先读取到
instance的旧值(nullptr),然后才执行create.exchange并发现create已经被设为true,于是进入循环等待。此时构造线程可能已经完成了instance.store(new widget(), memory_order_release),但等待线程因为提前读取了instance的旧值,后续的循环读取可能因为缓存一致性延迟,或者缺乏必要的同步关系,无法及时看到instance的更新值,从而陷入无限死循环。
而将create.exchange的内存序改为memory_order_acquire后,acquire语义会阻止任何后续的内存操作(包括instance.load)被重排到该操作之前,确保等待线程一定是先确认create已经被设为true,再去读取instance的值。这样当构造线程完成instance.store后,等待线程的instance.load(acquire)能正确与该store的release语义建立同步关系,及时读取到更新后的instance值,避免死循环。
另外,原论点中“所有非nullptr的读取都会与该写入同步”的结论本身没错,但问题是在BAD代码中,等待线程可能永远无法读取到非nullptr的instance值,因为指令重排序导致它陷入循环读取旧的nullptr。
内容的提问来源于stack exchange,提问作者user3188445
相关产品推荐
相关产品推荐

