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