为什么 std::call_once 在连接线程和分离线程中表现不同?
Why std::call_once behave different in joined and detached threads?
我写了一个小测试项目来查看 std::call_once 在执行 callable 时是否会阻塞。
项目的输出允许假设 call_once 有两种行为:它 在分离的 线程上阻塞并且 没有在加入。
我强烈怀疑这不可能是真的,但我无法得出其他结论,请指导我正确的。
using namespace std;
once_flag f;
mutex cout_sync;
void my_pause()
{
volatile int x = 0;
for(int i=0; i<2'000'000'000; ++i) { x++; }
}
void thr(int id)
{
auto start = chrono::system_clock::now();
call_once(f, my_pause);
auto end = chrono::system_clock::now();
scoped_lock l{cout_sync};
cout << "Thread " << id << " finished in " << (static_cast<chrono::duration<double>>(end-start)).count() << " sec" << endl;
}
int main()
{
vector<thread> threads;
for(int i=0; i<4; i++)
{
threads.emplace_back(thr, i);
threads.back().join();
}
return 0;
}
输出:
Thread 0 finished in 4.05423 sec
Thread 1 finished in 0 sec
Thread 2 finished in 0 sec
Thread 3 finished in 0 sec
将线程更改为分离:
for(int i=0; i<4; i++)
{
threads.emplace_back(thr, i);
threads.back().detach();
}
this_thread::sleep_for(chrono::seconds(5));
输出:
Thread 0 finished in 4.08223 sec
Thread 1 finished in 4.08223 sec
Thread 3 finished in 4.08123 sec
Thread 2 finished in 4.08123 sec
Visual Studio 2017
其实跟加入的版本是先加入线程,然后才开始下一个线程有关。
这些语义被触发是因为 specification of call_once:
If that invocation throws an exception, it is propagated to the caller
of call_once, and the flag is not flipped so that another call will be
attempted (such call to call_once is known as exceptional).
这意味着如果call_once的函数抛出异常,则不认为被调用,下一次调用call_once将再次调用该函数。
这意味着整个 call_once() 受到内部互斥体的有效保护。如果正在执行call_once-d函数,则任何其他进入call_once()的线程都必须被阻塞,直到call_once-d函数returns.
您一次加入一个线程,因此在第一个线程 call_once 已经返回之前不会调用第二个线程。
您同时有效地启动了所有四个分离的线程。实际上,所有四个线程将大约一起进入 call_once。
其中一个线程将最终执行调用的函数。
其他线程将被阻塞,直到被调用的函数 returns 或抛出异常。
这实际上意味着所有线程都必须等待。
这与分离线程无关。
如果您将代码的第一个版本更改为首先启动所有四个线程,然后将它们全部加入,您将看到相同的行为。
不同不一样
非分离线程按顺序 运行 — 代码会等待一个线程完成,然后再启动下一个线程。所以第一个进入等待循环,而其他的则没有。
同时分离的线程运行。其中一个 运行 是等待循环,其他的阻塞直到等待循环完成。
同时将非分离线程的代码更改为 运行。为此,将连接移到创建线程的循环之外。
我写了一个小测试项目来查看 std::call_once 在执行 callable 时是否会阻塞。 项目的输出允许假设 call_once 有两种行为:它 在分离的 线程上阻塞并且 没有在加入。 我强烈怀疑这不可能是真的,但我无法得出其他结论,请指导我正确的。
using namespace std;
once_flag f;
mutex cout_sync;
void my_pause()
{
volatile int x = 0;
for(int i=0; i<2'000'000'000; ++i) { x++; }
}
void thr(int id)
{
auto start = chrono::system_clock::now();
call_once(f, my_pause);
auto end = chrono::system_clock::now();
scoped_lock l{cout_sync};
cout << "Thread " << id << " finished in " << (static_cast<chrono::duration<double>>(end-start)).count() << " sec" << endl;
}
int main()
{
vector<thread> threads;
for(int i=0; i<4; i++)
{
threads.emplace_back(thr, i);
threads.back().join();
}
return 0;
}
输出:
Thread 0 finished in 4.05423 sec
Thread 1 finished in 0 sec
Thread 2 finished in 0 sec
Thread 3 finished in 0 sec
将线程更改为分离:
for(int i=0; i<4; i++)
{
threads.emplace_back(thr, i);
threads.back().detach();
}
this_thread::sleep_for(chrono::seconds(5));
输出:
Thread 0 finished in 4.08223 sec
Thread 1 finished in 4.08223 sec
Thread 3 finished in 4.08123 sec
Thread 2 finished in 4.08123 sec
Visual Studio 2017
其实跟加入的版本是先加入线程,然后才开始下一个线程有关。
这些语义被触发是因为 specification of call_once:
If that invocation throws an exception, it is propagated to the caller of call_once, and the flag is not flipped so that another call will be attempted (such call to call_once is known as exceptional).
这意味着如果call_once的函数抛出异常,则不认为被调用,下一次调用call_once将再次调用该函数。
这意味着整个 call_once() 受到内部互斥体的有效保护。如果正在执行call_once-d函数,则任何其他进入call_once()的线程都必须被阻塞,直到call_once-d函数returns.
您一次加入一个线程,因此在第一个线程 call_once 已经返回之前不会调用第二个线程。
您同时有效地启动了所有四个分离的线程。实际上,所有四个线程将大约一起进入 call_once。
其中一个线程将最终执行调用的函数。
其他线程将被阻塞,直到被调用的函数 returns 或抛出异常。
这实际上意味着所有线程都必须等待。
这与分离线程无关。
如果您将代码的第一个版本更改为首先启动所有四个线程,然后将它们全部加入,您将看到相同的行为。
不同不一样
非分离线程按顺序 运行 — 代码会等待一个线程完成,然后再启动下一个线程。所以第一个进入等待循环,而其他的则没有。
同时分离的线程运行。其中一个 运行 是等待循环,其他的阻塞直到等待循环完成。
同时将非分离线程的代码更改为 运行。为此,将连接移到创建线程的循环之外。