gpt4 book ai didi

c++ - 所有用例的双重检查锁是否都已损坏?

转载 作者:行者123 更新时间:2023-12-03 12:46:09 25 4
gpt4 key购买 nike

我知道单例惰性初始化的双重检查锁定被破坏了:

// SingletonType* singleton; 
// std::mutex mtx;
SingletonType* get()
{
if(singleton == nullptr){
lock_guard _(mtx);
if(singleton == nullptr) {
singleton = new SingleTonType();
}
}
return singleton;
}

上面的代码被打破了,因为指令可能会被重新安排,所以指向单例的指针赋值可能发生在 SingletonType 构造函数调用之前或期间,因此当线程 1 获取锁并初始化时,线程 2 可能会看到单例不再为空,同时构造 new SingleTon() 尚未完成,导致未定义的行为。

基于这种理解,我认为 double-check-lock broken 仅在单例初始化情况下被破坏。例如,我认为以下用法是安全的。这种理解是否正确?

// 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;
}
}
}

最佳答案

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

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
}
}

关于c++ - 所有用例的双重检查锁是否都已损坏?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65650844/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com