【C/C++】双重检查锁定下的atomic操作

文章目录

  • DCL & atomic
    • 1 “双重检查锁定”(Double-Checked Locking, DCL)模式
    • 2 外层的 `atomic.load()` 真能拦住多线程吗?
    • 3 原因详解
    • 4 关键点总结
    • 5 推荐替代方式
    • 6 总结

DCL & atomic

1 “双重检查锁定”(Double-Checked Locking, DCL)模式

这个模式主要用于“懒汉式”初始化单例,结构如下:

std::atomic<MySingleton*> instance = nullptr;
std::mutex mtx;

MySingleton* getInstance() {
    MySingleton* tmp = instance.load(std::memory_order_acquire);  // 第一次检查(无锁)
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(mtx);
        tmp = instance.load(std::memory_order_relaxed);           // 第二次检查(加锁后)
        if (tmp == nullptr) {
            tmp = new MySingleton();                              // 初始化
            instance.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

2 外层的 atomic.load() 真能拦住多线程吗?

不能完全拦住多线程同时进入 if 判断,但它是有意义的。
真正的防止并发初始化,靠的是加锁 + 第二次判断(内层 check)。


3 原因详解

外层 load() 是一种“快速路径”优化:

  • 外层 load() 是非阻塞的、快速的:它会拦住大部分非首次访问的线程,避免加锁;
  • 但在第一次初始化时,多个线程同时进入 if (tmp == nullptr) 是完全可能的,除非你加锁。

所以外层 atomic.load() 不是“拦住线程”,而是:

  • 提前判断,避免争抢锁;
  • 减少后续加锁压力;
  • 真正的并发安全靠 mutex + 第二次检查。

4 关键点总结

层级 操作 能力
外层 atomic.load 快速判断 ❌ 不能完全阻止并发初始化
mutex + 内层检查 严格保护 ✅ 保证只有一个线程初始化
store(release) / load(acquire) 保证内存可见性 ✅ 保证其他线程看到完整对象

C++11 之后 DCL 是安全的,但要求:

  1. instancestd::atomic
  2. 使用 memory_order_acquire / release 保证内存顺序。
  3. 避免指针“构造中被读取”(见下)。

DCL 的历史 bug(构造未完成)

如果你写的是:

MySingleton* tmp = new MySingleton(); // tmp 构造中
instance.store(tmp);                 // 此时 store 了一个尚未完整构造的对象!

另一个线程可能这样:

MySingleton* p = instance.load();    // 获取到指针
p->doSomething();                    //  使用未初始化完的对象!

所以 C++11 要求:用 store(memory_order_release) + load(memory_order_acquire) 保证:

  • store 之前的所有写(对象构造)对 load 之后可见;
  • 防止指令重排导致“构造未完成就可见”。

5 推荐替代方式

在现代 C++ 中,推荐更安全的方式:

使用 std::call_once + std::once_flag

std::once_flag flag;
MySingleton* instance = nullptr;

MySingleton* getInstance() {
    std::call_once(flag, []() {
        instance = new MySingleton();  // 构造只会被调用一次
    });
    return instance;
}

这是最安全、最简洁的单例方式。


6 总结

  • 外层 atomic.load() 并不能“拦住多线程”,它只是优化路径。
  • 真正防止多线程同时初始化靠的是 锁 + 第二次检查。
  • DCL 在 C++11 后 配合 acquire/release 语义是安全的,但实现时要特别小心构造顺序。
  • std::call_once 是更现代的、安全的替代方案。

你可能感兴趣的:(C/C++,c语言,c++)