如何实现完全符合C++标准、无需alignas及前置假设的双重检查锁?
关于C++局部静态变量双检锁实现的标准合规性问题
我在cppreference上看到这么一句话:
注意:该特性的常见实现采用双检锁模式的变体,将已初始化局部静态变量的运行时开销降低为一次非原子布尔比较。
之后我尝试不使用std::call_once自行实现这个特性,但发现很难做到完全符合C++标准。我最终的实现代码如下,但这个实现实际上依赖了「基于缓存同步的内存序」概念:
#include <iostream> #include <mutex> #include <atomic> template<typename T,auto = nullptr,size_t = 0> class Static { struct alignas(std::hardware_destructive_interference_size) // 防止bool变量被独立同步 Releaser { union { char _place_holder{}; T value; }; bool constructed{}; ~Releaser() { if (constructed) { value.~T(); } } }; inline static std::mutex s_m; inline static Releaser s_value; public: static T &get() { if (s_value.constructed) { return s_value.value; } { std::lock_guard l(s_m); if (s_value.constructed) { return s_value.value; } new (&s_value.value) T(); std::atomic_thread_fence(std::memory_order_acq_rel); s_value.constructed = true; } return s_value.value; } }; int main() { auto& a = Static<std::string,main>::get(); a = "1"; std::cout << Static<std::string,main>::get(); }
我甚至不确定这个实现能否适配多数平台,想请教是否存在无需alignas及额外前置假设的纯C++标准实现方案。
标准合规的实现方案
要实现符合C标准的线程安全懒加载局部静态变量(等价于C11及以后static T x; return x;的原生行为),且不依赖std::call_once,核心是严格遵循C++内存模型,用原子操作保证可见性,避免未定义行为(UB)。原实现的核心问题在于普通bool的跨线程读写属于UB,即使加alignas也无法解决。
以下是纯标准实现,无需依赖任何平台特定假设:
#include <iostream> #include <mutex> #include <atomic> #include <string> template<typename T, auto Tag = nullptr, size_t N = 0> class Static { struct Releaser { union { char placeholder{}; T value; }; std::atomic<bool> constructed{false}; ~Releaser() { if (constructed.load(std::memory_order_acquire)) { value.~T(); } } }; inline static std::mutex s_mutex; inline static Releaser s_value; public: static T& get() { // 第一次检查:acquire序确保读到true时,初始化的副作用完全可见 if (s_value.constructed.load(std::memory_order_acquire)) { return s_value.value; } std::lock_guard<std::mutex> lock(s_mutex); // 第二次检查:持有锁时用relaxed序即可,锁本身提供acquire/release语义 if (s_value.constructed.load(std::memory_order_relaxed)) { return s_value.value; } // 原地构造对象 new (&s_value.value) T(); // release序确保初始化操作不会被重排到store之后 s_value.constructed.store(true, std::memory_order_release); return s_value.value; } }; int main() { auto& a = Static<std::string, main>::get(); a = "1"; std::cout << Static<std::string, main>::get() << std::endl; return 0; }
实现要点说明
- 原子变量强制可见性:将
constructed改为std::atomic<bool>,所有读写都是原子操作,彻底避免非原子变量跨线程访问的UB。 - 内存序精准控制:
- 第一次检查用
memory_order_acquire:保证如果读到true,之前的初始化操作(T的构造)对当前线程完全可见。 - 初始化后的
store(true, memory_order_release):保证构造T的所有操作不会被编译器或CPU重排到store之后,其他线程通过acquire加载能看到完整的对象。 - 第二次检查用
memory_order_relaxed:因为持有互斥锁,锁的lock(acquire)和unlock(release)已经提供了内存屏障,这里的加载不需要额外约束,兼顾性能。
- 第一次检查用
- 无需alignas:
alignas是用于优化伪共享的性能手段,不是标准合规的必要条件。如果追求性能可以保留,但不影响正确性。 - 析构安全:析构函数中用
acquire加载constructed,确保析构时对象状态完整,避免UB。
这个实现完全基于C标准的原子操作和内存模型,可移植到所有符合C11及以后标准的平台。
内容的提问来源于stack exchange,提问作者余国良
相关产品推荐
相关产品推荐

