You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++中使用double-check lock(双检查锁)是否存在潜在问题?

提问:C++双检查锁实现的正确性疑问

以下为演示用的简单代码片段:
有人告知我双检查锁的写法是错误的,因为变量未声明为volatile,编译器可自由对调用指令进行重排序或者将相关操作优化掉。
但我确实在很多项目中见过这类代码的应用,希望有人能帮忙解答这个疑问。我已经搜索过相关资料也和朋友讨论过,仍然没有得到明确答案。

#include <iostream>
#include <mutex>
#include <fstream>

namespace DemoLogger
{
    void InitFd()
    {
        if (!is_log_file_ready)
        {
            std::lock_guard<std::mutex> guard(log_mutex);
            if (!is_log_file_ready)
            {
                log_stream.open("sdk.log", std::ofstream::out | std::ofstream::trunc);
                is_log_file_ready = true;
            }
        }
    }


    extern static bool is_log_file_ready;
    extern static std::mutex log_mutex;
    extern static std::ofstream log_stream;
}

//cpp
namespace DemoLogger
{
    bool is_log_file_ready{false};
    std::mutex log_mutex;
    std::ofstream log_stream;
}

更新:感谢各位的回复,确实存在InitFd()的更优实现,但以上仅为简单示例,我真正想要了解的就是双检查锁本身是否存在潜在问题。


回答

上述双检查锁实现确实存在并发安全隐患,核心问题来自指令重排序:

  • 在未加任何内存同步语义的前提下,编译器、CPU都有权对指令执行顺序进行重排序优化,可能出现is_log_file_ready = true的赋值操作,被重排序到log_stream.open执行完成之前。此时如果有其他线程刚好在第一次if (!is_log_file_ready)判断时读到了为true的标志位,就会误认为日志文件已经初始化完成,直接使用还未完成open操作的log_stream,触发未定义行为。
  • 很多旧资料提到的添加volatile关键字的解决方法对C多线程场景无效:C标准中volatile仅用于阻止编译器对变量的读写优化(比如将变量缓存到寄存器,省略内存读写),不提供内存屏障语义,既无法阻止CPU层面的指令重排序,也不能保证多线程场景下的变量可见性,无法解决双检查锁的安全问题。

很多项目中这类写法能正常运行,通常是以下几个原因:

  1. 运行架构为x86,x86属于强内存模型,默认不会对写-写操作进行重排序,刚好避开了这个场景的重排序问题,但这种依赖硬件特性的代码是不跨平台的,不符合C++标准规范
  2. 初始化逻辑仅在程序启动早期的单线程阶段执行,没有真正触发多线程并发调用InitFd的场景
  3. 编译器做了特定实现的额外保证,但这不属于通用标准的范畴,换编译器或者编译选项就可能出问题

C++11及以后标准下,正确的双检查锁实现需要用std::atomic提供内存同步语义,将is_log_file_ready的定义改为std::atomic<bool> is_log_file_ready{false},同时修改判断和赋值逻辑的内存序即可保证安全:

void InitFd()
{
    // acquire内存序保证后续读写不会重排到本次load之前
    if (!is_log_file_ready.load(std::memory_order_acquire))
    {
        std::lock_guard<std::mutex> guard(log_mutex);
        if (!is_log_file_ready.load(std::memory_order_relaxed))
        {
            log_stream.open("sdk.log", std::ofstream::out | std::ofstream::trunc);
            // release内存序保证前面的所有写操作都完成后,才会执行本次store
            is_log_file_ready.store(true, std::memory_order_release);
        }
    }
}

如果只是要实现线程安全的单次初始化,C++11及以后标准已经保证函数内静态局部变量的初始化是线程安全的,完全不需要手动实现双检查锁,写法更简单也更安全:

std::ofstream& GetLogStream()
{
    static std::ofstream log_stream("sdk.log", std::ofstream::out | std::ofstream::trunc);
    return log_stream;
}

内容的提问来源于stack exchange,提问作者John

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 17:39:01