【问题标题】:std::call_once vs std::mutex for thread-safe initializationstd::call_once vs std::mutex 用于线程安全初始化
【发布时间】:2018-05-31 23:19:15
【问题描述】:

我对@9​​87654322@ 的用途有点困惑。需要明确的是,我完全了解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 在普通互斥体之上提供了什么?

【问题讨论】:

  • 你试过/测试了吗?
  • @Brandon,测试竞争条件可能不切实际。
  • 为什么在第二个示例中调用lock.unlock()

标签: c++ multithreading c++11


【解决方案1】:

call_once 为您做的一件事是处理异常。也就是说,如果进入它的第一个线程在函子内抛出异常(并将其传播出去),call_once 将不会认为call_once 满足。允许随后的调用再次进入函子,以无例外地完成它。

在您的示例中,例外情况也得到了妥善处理。然而,很容易想象一个更复杂的函子,其中的例外情况不会得到正确处理。

说了这么多,我注意到call_once 与函数局部静态是多余的。例如:

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 的示例,恕我直言,更简单。哦,除了这个和你的例子之间的破坏顺序有很大的不同。在这两种情况下,m_instance 都以相反的构造顺序被销毁。但构建顺序不同。在您的 m_instance 中,是相对于同一翻译单元中具有文件本地范围的其他对象构造的。使用 function-local-statics,m_instance 在第一次执行 GetInstance 时被构造。

这种差异对您的应用程序可能很重要,也可能不重要。一般来说,我更喜欢函数局部静态解决方案,因为它是“懒惰的”。 IE。如果应用程序从不调用GetInstance(),则永远不会构造m_instance。并且在应用程序启动期间没有一段时间试图一次构建大量静态。您只需在实际使用时支付建设费用。

【讨论】:

  • 但是你的例子是如何使用函数本地静态线程安全的?
  • C++11 指定它是线程安全的。我知道是因为我必须为 libc++abi 实现它。 :-) 有关规范,请参见 C++11 的 6.7 [stmt.dcl]/p4。警告:VS-2013 还没有实现函数局部静态的线程安全构造。但其他人都这样做。
  • 哇。 C++11 确实解决了我们在 C++03 中必须处理的关于线程安全的许多问题。我已经习惯于看到函数局部静态并思考“OMG NOT THREAD SAFE!”
  • 请注意,GCC 支持编译器标志来禁用线程安全本地静态的可憎,并注意有些人(例如我)已将其添加到默认编译器选项中。使本地静态线程安全,而无需传达不希望的方式,这是一个非常愚蠢的设计决定。它主要有助于将罕见的错误和不正确代码的sn-ps转换为错误但正确代码,这仍然很糟糕。因此call_once 确实有它的优点。当且仅当您真的需要它时,它才提供强有力的保证,而不是当您实际上不想要它时。不过,很好的答案:)
  • @Damon:哇,你是我听到的第一个说你不喜欢函数局部静态的线程安全初始化的人。我必须说我在工作和个人项目中一直使用它们。在工作中移植到 VS-2015 之前,解决它们的不足是一个主要的问题。不过谢谢分享。
【解决方案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) 的情况下是安全的,并且所有主要编译器都支持这一点
  • 里面是一个保证只被调用一次的 lambda
  • 这种模式也可以安全地用于仅标题的情况

【讨论】:

    【解决方案3】:

    如果您阅读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 的调用完成之前没有线程可以继续。

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

    【讨论】:

    • 不保证线程安全? Executes the Callable object f exactly once, even if called from several threads.
    • 我很困惑。您粘贴的链接似乎与您所说的相反:No invocation in the group returns before the abovementioned execution of the selected function is completed successfully, that is, doesn't exit via an exception.我误会了吗?
    • @BryanChen 是的,但不保证不会出现数据竞争。
    • 你能举个例子吗?
    • 我也将该链接视为保证互斥锁的行为。更简洁地说,就像 C++11 的 std::remove_if (以及其他一百万种东西)不会为您提供您无法更详细获得的功能。
    猜你喜欢
    • 1970-01-01
    • 2014-05-06
    • 1970-01-01
    • 2018-05-11
    • 1970-01-01
    • 1970-01-01
    • 2013-12-29
    • 1970-01-01
    • 2021-08-11
    相关资源
    最近更新 更多