如何解决所有用例的双重检查锁都被破坏了吗?
我知道单例延迟初始化会破坏双重检查锁定:
// SingletonType* singleton;
// std::mutex mtx;
SingletonType* get()
{
if(singleton == nullptr){
lock_guard _(mtx);
if(singleton == nullptr) {
singleton = new SingletonType();
}
}
return singleton;
}
以上被破坏,因为指令可能会重新排列,因此可能在 SingletonType 构造函数调用之前或期间发生对单例的指针分配,因此当线程 1 获取锁并初始化时,线程 2 可能会在构造 { 时看到单例不再为空{1}} 尚未完成,导致未定义的行为。
根据这种理解,我认为双重检查锁定损坏仅在单例初始化情况下才损坏。例如,我认为以下用法是安全的。这种理解是否正确?
new Singleton()
解决方法
这里唯一未定义的行为是您从一个指针读取,然后在没有同步的情况下在不同的线程上写入它。现在这可能适用于大多数指针(特别是如果写入指针是原子的),但您可以很容易地明确:
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,__ATOMIC_RELAXED);
if (!result) {
result = new SingletonType();
__atomic_store(&singleton,__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,__ATOMIC_RELAXED);
if (local == 0) return;
if (local % threshold == 0) {
const std::lock_guard<std::mutex> _(mtx);
// Do periodic job every 1000 id interval
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。