std::call_once是线程安全的,但是它也可以重入吗?
我使用VS2012(Debug& Release)进行的测试表明,从单个线程递归调用std::call_once
是可以的,但如果调用是在不同的线程上进行的,则会导致死锁。这是std::call_once
的已知限制吗?
#include "stdafx.h"
#include <iostream>
#include <mutex>
#include <thread>
void Foo()
{
std::cout << "Foo start" << std::endl;
std::once_flag flag;
std::call_once( flag, [](){
std::cout << "Hello World!" << std::endl;
});
std::cout << "Foo end" << std::endl;
}
int _tmain(int argc, _TCHAR* argv[])
{
// Single threaded Works
{
std::once_flag fooFlag;
std::call_once( fooFlag, Foo);
}
// Works
// Threaded version, join outside call_once
{
std::once_flag fooFlag;
std::thread t;
std::call_once( fooFlag, [&t](){
t = std::thread(Foo);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
});
t.join();
}
// Dead locks
// Threaded version, join inside call_once
{
std::once_flag fooFlag;
std::call_once( fooFlag, [](){
auto t = std::thread(Foo);
t.join();
});
}
return 0;
}
似乎std:call_once
锁定了一个静态互斥锁,在该函数退出之前,该互斥锁无法解锁。在单线程情况下它可以工作,因为在第二次调用时线程已经有锁。在线程版本上,它将阻塞,直到第一个调用退出。
我还注意到,如果将std::once_flag
函数中的Foo()
标志更改为static
,则仍会发生死锁。
答案 0 :(得分:6)
最接近标准的是17.6.5.8 [reentrancy] :
1 - 除非在本标准中明确指定,否则它是实现定义的,可以递归地重新输入标准C ++库中的函数。
不幸的是call_once
的规范并没有说它是递归的(还是跨线程递归的),并且线程支持库前导码也没有对这个主题说什么。
也就是说,VC ++实现显然不是最理想的,特别是因为可以使用call_once
编写用户级版本的condition_variable
:
#include <mutex>
#include <condition_variable>
struct once_flag {
enum { INIT, RUNNING, DONE } state = INIT;
std::mutex mut;
std::condition_variable cv;
};
template<typename Callable, typename... Args>
void call_once(once_flag &flag, Callable &&f, Args &&...args)
{
{
std::unique_lock<std::mutex> lock(flag.mut);
while (flag.state == flag.RUNNING) {
flag.cv.wait(lock);
}
if (flag.state == flag.DONE) {
return;
}
flag.state = flag.RUNNING;
}
try {
f(args...);
{
std::unique_lock<std::mutex> lock(flag.mut);
flag.state = flag.DONE;
}
flag.cv.notify_all();
}
catch (...) {
{
std::unique_lock<std::mutex> lock(flag.mut);
flag.state = flag.INIT;
}
flag.cv.notify_one();
throw;
}
}
请注意,这是一个细粒度的实现;它也可以编写一个粗粒度的实现,它在所有一次标志中使用一对互斥和条件变量,但是你需要确保在抛出异常时通知所有等待的线程(例如, libc ++就是这样做的。)
为了提高效率,你可以使once_flag::state
成为原子并使用双重检查锁定;这里为了简洁而省略了。
答案 1 :(得分:0)
不是&#34;答案&#34;在这里,但至少确认Visual Studio方面的某些内容与您的使用模式有关。
我在coliru.stacked-crooked.com上编译并运行了这个示例,它似乎按预期执行,没有任何死锁。
这是编译行:
g++-4.8 -std=c++11 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
这是输出:
Foo start
Hello World!
Foo end
Foo start
Hello World!
Foo end
Foo start
Hello World!
Foo end
在我指责库实现之前,我个人认为我自己的代码存在问题。
我确实看到有一些类似的问题w.r.t. Visual Studio的call_once实现:
http://www.unknownerror.org/Problem/index/1100762158/why-does-this-c-static-singleton-never-stop/
我很想知道调试或发布中是否出现此问题,或者两者兼而有之?
ISO标准确实声明call_once毕竟是线程安全的!