使用原子的线程安全单例

Posted

技术标签:

【中文标题】使用原子的线程安全单例【英文标题】:Thread-safe singleton using atomic 【发布时间】:2020-07-22 07:35:28 【问题描述】:

只想知道atomic_flag = 1; 是否保持 myclass_st 分配线程安全。 (不好意思我的例子有这么多问题,所以改了。)

myClass* myclass_st = nullptr;
std::atomic<int> atomic_flag;
std::mutex mtx; //err
myClass* get_instance() 
    
    //std::unique_lock<std::mutex> lk(mtx);
    if (myclass_st == nullptr) 
        mtx.lock();
        if (myclass_st == nullptr) 
            myclass_st = new myClass();
            atomic_flag = 1;
            mtx.unlock(); //err
        
    
    return myclass_st;


我知道我们可以在 c11 之后使用static

也许我应该像这样修改代码?

myClass* myclass_st = nullptr;
std::atomic<int> atomic_flag;
myClass* get_instance() 

    if (atomic_flag.load() == 0) 
        std::unique_guard<std::mutex> lk(mtx);
        if (atomic_flag.load() == 0) 
            myclass_st = new myClass();
            atomic_flag = 1;
        
    
    return myclass_st;

【问题讨论】:

在 unique_lock 之后,互斥锁已经被锁定。您的显式锁定和解锁不一致。 unique_lock 更好,因为一旦抛出异常或超出范围,unique_lock 将展开并解锁线程。但是std::mutex 不是。 互斥锁对线程安全没有任何作用,每次调用get_instance都会生成一个新的mtx,所以它不会同步任何东西。另外,如果从未检查过,为什么还要使用原子标志? 您正在检查非原子myclass_st,而另一个线程(已锁定互斥锁)可以修改它。你需要检查atomic_flag 您为什么认为atomic_flag = 1 会使myclass_st 分配线程安全? 【参考方案1】:

显示的代码不是线程保存的,因为std::mutex mtx; 每次都是一个新对象。 std::mutex mtx; 必须是 static so that it is the same mutex for each invocation of get_instance`,但即便如此,手动锁定和解锁互斥锁在当前表单中也无效。

EDITstd::mutex mtx; 移出函数后,对于get_instance 的每次调用,互斥锁都是相同的。但它仍然不是线程保存。多个线程可以通过第一个if (myclass_st == nullptr) 条件,其中myclass_stnullptr。如果有例如超过三个线程通过第一个if 然后第一个调用lock 的线程将设置myclass_st 并释放它对互斥锁的锁定。调用lock 的第二个线程不会释放其获得的锁,因此通过第一个if 的所有其他线程都将被阻塞。

必须是:

myClass* get_instance() 
    mtx.lock();
    if (myclass_st == nullptr) 
      myclass_st = new myClass();
      atomic_flag = 1;
    
    mtx.unlock();
    return myclass_st;

您通常希望使用具有自动存储持续时间(RAII 习惯用法)的锁守卫来进行锁定,而不是手动锁定和解锁,因为这样可以确保在离开 get_instance 时始终释放互斥锁上的锁。

myClass* get_instance() 
    std::lock_guard<std::mutex> lk(mtx);
    if (myclass_st == nullptr) 
      myclass_st = new myClass();
      atomic_flag = 1;
    
    return myclass_st;

编辑 不,第一个和第二个示例都不是线程保存。对于您展示的第二个示例,if (atomic_flag.load() == 0) /** ... **/ atomic_flag = 1; 仍然可以由两个线程输入。所以new myClass 仍然可以重复多次。

【讨论】:

第二个例子是最佳实践 (RAII)。如果new 失败或构造函数失败,第一个示例将无法解锁mtx 谢谢。这是安全的。但是每个线程都必须获得互斥锁。成本很高。 @Anna 这不是你最初的问题。正如我在评论中已经提到的那样,您不断改变您的问题及其范围。最初,您询问您在初始问题中显示的代码和第一次编辑是否是线程保存。 Err.. 我只是想专注于如何使 new 原子化。有些问题与我想知道的无关。所以我改变了。我会换个问题。谢谢。【参考方案2】:

只想知道 atomic_flag = 1;保持 myclass_st 分配线程安全与否。

没有。

也许我应该像这样修改代码?

myClass* myclass_st = nullptr;
std::atomic<int> atomic_flag;
myClass* get_instance() 

    if (atomic_flag.load() == 0) 
        myclass_st = new myClass();
        atomic_flag = 1;
    
    return myclass_st;

如果你打算 get_instance 被多个线程调用,那么不会。

我猜你想要:

myClass* myclass_st = nullptr;
std::atomic<int> atomic_flag0;
std::mutex mtx;
myClass* get_instance() 
    if (atomic_flag == 0) 
        std::unique_lock<std::mutex> lk(mtx);
        if (myclass_st == nullptr) 
            myclass_st = new myClass();
            atomic_flag = 1;
        
    
    return myclass_st;

【讨论】:

以上是关于使用原子的线程安全单例的主要内容,如果未能解决你的问题,请参考以下文章

volatile关键字

线程安全的分析--finalstatic单例线程安全

Java中如何保证线程安全性

怎么实现一个线程安全的单例模式

java 单例 线程安全 写一个测试类 说明下面单例不是线程安全的

LINUX多线程(线程池,单例模式,线程安全,读者写者模型)