我对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
在普通互斥体之上提供了什么呢?
答案 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
请注意
答案 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 called
和g() was called
。这是因为在std::call_once
的正文中,它将检查是否设置了flag
然后设置它,如果没有则调用适当的函数。但是在检查时或者在设置flag
之前,另一个线程可以使用相同的标志调用call_once
并同时运行一个函数。如果您知道另一个线程可能有数据争用,您仍应使用互斥锁保护对call_once
的调用。
我找到了std::call_once
函数和线程库proposal的链接,该文件声明并发性保证只调用一次函数,所以它应该像互斥锁(y)一样工作
更具体地说:
如果多个具有相同标志的call_once调用在不同的线程中同时执行,那么只有一个线程应该调用func,并且在调用func完成之前不会继续执行任何线程。
所以回答你的问题:是的,其他线程将被阻塞,直到调用线程从指定的仿函数返回。