现代C++无锁线程安全懒式手动单例的内存顺序正确性验证
原子变量实现无锁单例的内存顺序正确性验证
我知道单例模式并非理想方案,现代C++中通常可借助静态局部变量实现线程安全的懒式初始化。但因特定场景需求,尽管我可采用更安全但性能稍逊的带互斥量双重检查锁技术,仍希望验证自己对原子变量及内存顺序的理解。我编写了一段自认为无需强内存屏障的性能最优无锁实现代码,看似可行,但想确认内存顺序的使用是否正确、能否达到预期行为。
代码如下:
// in header file class DLL_EXPORT MyLogger { public: void logMessage(std::string message); private: static std::atomic<MyLogger*> sSharedInstance; }; // in cpp file constinit std::atomic<MyLogger*> MyLogger::sSharedInstance{nullptr}; MyLogger& MyLogger::sharedInstance() { MyLogger* logger = sSharedInstance.load(std::memory_order_relaxed); if (!logger) { logger = new MyLogger(); // 构造函数为空操作,冗余实例不会造成问题 MyLogger* previousLogger = nullptr; if (!sSharedInstance.compare_exchange_strong(previousLogger, logger, std::memory_order_relaxed, std::memory_order_relaxed)) { // 初始化竞争失败 - 销毁冗余实例 delete logger; logger = previousLogger; } } return *logger; } // 在不同线程中调用 void someFunction() { // 可被多线程调用... MyLogger::sharedInstance().logMessage("some message"); }
内存顺序与代码正确性分析
基于你构造函数为**空操作(no-op)**这个核心前提,你的代码内存顺序使用是正确的,能达到预期的无锁懒初始化效果:
- load操作的relaxed顺序:因为构造函数无任何初始化逻辑,不存在实例初始化操作与指针赋值的指令重排风险。原子变量自身的一致性保证了,当CAS成功后,其他线程最终能看到这个非空指针值,relaxed顺序不会影响指针本身的可见性,同时避免了强内存屏障的性能开销。
- compare_exchange_strong的relaxed顺序:
- CAS成功时,由于构造函数无操作,不需要同步任何实例初始化的内存操作,relaxed顺序完全足够;
- CAS失败时,
previousLogger会被原子更新为当前的实例指针,我们只需要这个指针的值本身,无需同步其他内存,所以relaxed顺序也没问题。
- 预期行为达成:多线程竞争初始化时,最多会创建多个冗余实例,但都会被销毁,最终所有线程都会拿到同一个实例的引用,且不会出现使用未初始化实例的情况(因为构造无操作)。
关键注意事项
这个实现的正确性完全依赖构造函数为空操作的前提:如果后续给构造函数添加了任何初始化逻辑(比如成员变量赋值、资源分配),当前的relaxed内存顺序会导致严重问题——其他线程可能看到非空指针,但实例的初始化操作还未完成,进而触发未定义行为。这种情况下需要调整内存顺序:将load改为std::memory_order_acquire,CAS成功时的顺序改为std::memory_order_release,以此保证构造操作的内存可见性。
内容的提问来源于stack exchange,提问作者The Storm
相关产品推荐
相关产品推荐

