所有用例的双重检查锁都坏了吗?

Posted

技术标签:

【中文标题】所有用例的双重检查锁都坏了吗?【英文标题】:Is double check lock broken for all use case? 【发布时间】:2021-01-10 07:04:04 【问题描述】:

我了解单例延迟初始化会破坏双重检查锁定:

// SingletonType* singleton; 
// std::mutex mtx;
SingletonType* get()

  if(singleton == nullptr)
    lock_guard _(mtx);
    if(singleton == nullptr) 
      singleton = new SingleTonType();
    
  
  return singleton;

上面的错误是因为指令可能被重新排列,所以单例的指针分配可能发生在 SingletonType 构造函数调用之前或期间,因此当线程 1 获取锁并初始化时,线程 2 可能在构造 @ 时看到单例不再为空987654322@ 尚未完成,导致未定义的行为。

有了这个理解,我认为双重检查锁被破坏只在单例初始化的情况下被破坏。例如,我认为以下用法是安全的。这种理解正确吗?

// int id = 0; 
// int threshold = 1000;
// std::mutex mtx;

void increment()

   int local = __atomic_fetch_add(&id, 1l, __ATOMIC_RELAXED);
   if(local >= threshold) 
      const std::lock_guard<std::mutex> _(mtx);
      if(local >= threshold) 
        // Do periodic job every 1000 id interval
        threshold += 1000;
      
   

【问题讨论】:

以我有限的知识我可以说你对你的singleton版本的理解是正确的。这就是为什么 Mayer 的单例更好。它的线程安全。 【参考方案1】:

这里唯一未定义的行为是您从指针中读取,然后在不同的线程上写入它而没有同步。现在这对大多数指针来说可能没问题(特别是如果写入指针是原子的),但你可以很容易地明确:

std::atomic<SingletonType*> singleton;
std::mutex mtx;

SingletonType* get()

    SingletonType* result = singleton.load(std::memory_order_relaxed);
    if (!result) 
        std::scoped_lock _(mtx);
        result = singleton.load(std::memory_order_relaxed);
        if (!result) 
            result = new SingletonType();
            singleton.store(result, std::memory_order_relaxed);
        
    
    return result;


// Or with gcc builtins
SingletonType* singleton;
std::mutex mtx;

SingletonType* get()

    SingletonType* result;
    __atomic_load(&singleton, &result, __ATOMIC_RELAXED);
    if (!result) 
        std::scoped_lock _(mtx);
        __atomic_load(&singleton, &result, __ATOMIC_RELAXED);
        if (!result) 
            result = new SingletonType();
            __atomic_store(&singleton, &result, __ATOMIC_RELAXED);
        
    
    return result;

不过,有一个更简单的实现方式:

SingletonType* get()

    static SingletonType singleton;
    return &singleton;
    // Or if your class has a destructor
    static SingletonType* singleton = new SingeltonType();
    return singleton;


这通常也被实现为双重检查锁(除了隐藏的isSingletonConstructed bool 而不是指针是否为空)


您最初的担心似乎是new SingletonType() 等效于operator new(sizeof(SingletonType)),然后在获取的存储上调用构造函数,并且编译器可能会在分配指针后重新排序调用构造函数。但是,不允许编译器重新排序分配,因为这会产生明显的影响(就像您注意到另一个线程在构造函数仍在运行时返回 singleton)。


您的increment 函数可以同时读取和写入threshold(在第一次检查双重检查锁和获取互斥锁并递增threshold += 1000 之后),因此它可能存在竞争条件。

你可以这样修复它:

void increment()

   int local = __atomic_fetch_add(&id, 1l, __ATOMIC_RELAXED);
   if (local >= __atomic_load_n(&threshold, __ATOMIC_RELAXED)) 
      const std::lock_guard<std::mutex> _(mtx);
      int local_threshold = __atomic_load_n(&threshold, __ATOMIC_RELAXED);
      if (local >= local_threshold) 
        // Do periodic job every 1000 id interval
        __atomic_store_n(&threshold, local_threshold + 1000, __ATOMIC_RELAXED);
      
   

但在这种情况下你并不真的需要原子,因为local 将是每个整数恰好一次(只要它只通过increment 修改),所以你可以改为:

// int id = 0; 
// constexpr int threshold = 1000;
// std::mutex mtx;  // Don't need if jobs can run in parallel

void increment()

   int local = __atomic_fetch_add(&id, 1l, __ATOMIC_RELAXED);
   if (local == 0) return;
   if (local % threshold == 0) 
      const std::lock_guard<std::mutex> _(mtx);
      // Do periodic job every 1000 id interval
   

【讨论】:

感谢详细的解释!我有一个重新升级实现的原子版本的问题: void increment() int local = __atomic_fetch_add(&id, 1l, __ATOMIC_RELAXED); if (local >= __atomic_load_n(&threshold, __ATOMIC_RELAXED)) const std::lock_guard<:mutex> _(mtx); int local_threshold = __atomic_load_n(&threshold, __ATOMIC_RELAXED); if (local >= local_threshold) // 每 1000 个 id 间隔执行周期性作业 __atomic_store_n(&threshold, local_threshold + 1000, __ATOMIC_RELAXED);

以上是关于所有用例的双重检查锁都坏了吗?的主要内容,如果未能解决你的问题,请参考以下文章

所有现有测试用例的代码覆盖率?

具有两个用例的类型提示功能

执行测试用例的几个注意

未通过所有测试用例的二叉树的最大路径

自动化测试用例的原子性#yyds干货盘点#

是否有一个插件或工具可用于在没有测试用例的情况下从正在运行的应用程序生成覆盖?