为什么 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 或抛出异常。

这实际上意味着所有线程都必须等待。

这与分离线程无关。

如果您将代码的第一个版本更改为首先启动所有四个线程,然后将它们全部加入,您将看到相同的行为。

不同不一样

非分离线程按顺序 运行 — 代码会等待一个线程完成,然后再启动下一个线程。所以第一个进入等待循环,而其他的则没有。

同时分离的线程运行。其中一个 运行 是等待循环,其他的阻塞直到等待循环完成。

同时将非分离线程的代码更改为 运行。为此,将连接移到创建线程的循环之外。