std :: call_once vs std :: mutex用于线程安全初始化

时间:2014-11-18 02:05:24

标签: c++ multithreading c++11

我对std::call_once的目的感到有点困惑。为了清楚起见,我完全理解std::call_once 做了什么,以及如何使用它。它通常用于原子初始化某个状态,并确保只有一个线程初始化状态。我也在网上看到很多尝试用std::call_once创建一个线程安全的单例。

作为 demonstrated here ,假设你编写一个线程安全的单例,如下:

CSingleton& CSingleton::GetInstance()
{
    std::call_once(m_onceFlag, [] {
        m_instance.reset(new CSingleton);
    });
    return *m_instance.get();
}

好的,我明白了。但我认为std::call_once唯一真正保证的是传递的函数执行一次。但是否保证如果在多个线程之间调用该函数并且一个线程获胜,则其他线程将阻止直到获胜线程从该线程返回调用

因为如果是这样,我认为call_once和普通同步互斥锁之间没有区别,例如:

CSingleton& CSingleton::GetInstance()
{
    std::unique_lock<std::mutex> lock(m_mutex);
    if (!m_instance)
    {
      m_instance.reset(new CSingleton);
    }
    lock.unlock();

    return *m_instance;
}

那么,如果std::call_once确实迫使其他线程阻塞,那么std::call_once对常规互斥锁有什么好处呢?再考虑一下,std::call_once肯定强制其他线程阻塞,或者在用户提供的函数中完成的任何计算都不会被同步。那么,std::call_once在普通互斥体之上提供了什么呢?

3 个答案:

答案 0 :(得分:13)

call_once为您做的一件事是处理异常。也就是说,如果它的第一个线程在仿函数内部引发异常(并将其传播出去),call_once将不会认为call_once满意。允许后续调用再次进入仿函数,以便在没有例外的情况下完成它。

在您的示例中,例外情况也得到了妥善处理。然而,很容易想象一个更复杂的仿函数,其中特殊情况将无法妥善处理。

所有这些都说,我注意到call_once对于函数本地静态是多余的。 E.g:

CSingleton& CSingleton::GetInstance()
{
    static std::unique_ptr<CSingleton> m_instance(new CSingleton);
    return *m_instance;
}

或更简单:

CSingleton& CSingleton::GetInstance()
{
    static CSingleton m_instance;
    return m_instance;
}

以上相当于您使用call_once的示例,以及imho,更简单。哦,除了破坏的顺序在这和你的例子之间有微妙的差别。在这两种情况下,m_instance都以相反的构造顺序被销毁。但是建筑的顺序是不同的。在m_instance中,相对于同一翻译单元中具有文件本地范围的其他对象构造。使用function-local-statics,m_instance是在GetInstance第一次执行时构造的。

这种差异对您的申请可能重要,也可能不重要。一般来说,我更喜欢功能本地静态解决方案,因为它是&#34;懒惰&#34;。即如果应用程序从不调用GetInstance(),则永远不会构造m_instance。在应用程序启动期间没有任何时期可以立即构建大量静力学。只有在实际使用时才支付施工费用。

答案 1 :(得分:2)

标准C ++解决方案的轻微变化是在通常的内部使用lambda:

// header.h
namespace dbj_once {

    struct singleton final {};

    inline singleton & instance()
    {
        static singleton single_instance = []() -> singleton {
            // this is called only once
            // do some more complex initialization
            // here
            return {};
        }();
        return single_instance;
    };

 } // dbj_once

请注意

  • 匿名命名空间意味着内部变量的默认静态链接。因此,不要把它放在里面。这是标题代码。
  • 值得重复:这在多线程(MT)存在时是安全的,并且所有主要编译器都支持这样做
  • inside是一个lambda,保证只调用一次
  • 此模式在仅标题情况下也可以安全使用

答案 2 :(得分:1)

如果您阅读this,您会发现std::call_once无法保证数据争用,它只是一次执行操作的实用程序功能(可以跨线程工作)。你不应该认为它具有接近互斥体影响的任何东西。

作为一个例子:

#include <thread>
#include <mutex>

static std::once_flag flag;

void f(){
    operation_that_takes_time();
    std::call_once(flag, [](){std::cout << "f() was called\n";});
}

void g(){
    operation_that_takes_time();
    std::call_once(flag, [](){std::cout << "g() was called\n";});
}

int main(int argc, char *argv[]){
    std::thread t1(f);
    std::thread t2(g);
    t1.join();
    t2.join();
}

可以同时打印f() was calledg() was called。这是因为在std::call_once的正文中,它将检查是否设置了flag然后设置它,如果没有则调用适当的函数。但是在检查时或者在设置flag之前,另一个线程可以使用相同的标志调用call_once并同时运行一个函数。如果您知道另一个线程可能有数据争用,您仍应使用互斥锁保护对call_once的调用。

修改

我找到了std::call_once函数和线程库proposal的链接,该文件声明并发性保证只调用一次函数,所以它应该像互斥锁(y)一样工作

更具体地说:

  

如果多个具有相同标志的call_once调用在不同的线程中同时执行,那么只有一个线程应该调用func,并且在调用func完成之前不会继续执行任何线程。

所以回答你的问题:是的,其他线程将被阻塞,直到调用线程从指定的仿函数返回。