是否有必要使用std :: atomic来表示线程已完成执行?

时间:2013-01-16 18:52:50

标签: c++ c++11 stdthread stdatomic

我想检查std::thread是否已完成执行。搜索stackoverflow我发现以下question解决了这个问题。接受的答案建议让工作线程在退出之前设置变量并让主线程检查该变量。以下是此类解决方案的最小工作示例:

#include <unistd.h>
#include <thread>

void work( bool* signal_finished ) {
  sleep( 5 );
  *signal_finished = true;
}

int main()
{
  bool thread_finished = false;
  std::thread worker(work, &thread_finished);

  while ( !thread_finished ) {
    // do some own work until the thread has finished ...
  }

  worker.join();
}

对收到的答案发表评论的人声称,人们不能使用简单的bool变量作为信号,代码在没有内存障碍的情况下被破坏,使用std::atomic<bool>将是正确的。我最初的猜测是这是错误的,简单的bool就足够了,但我想确保我没有错过任何东西。 上述代码是否需要std::atomic<bool>才能正确使用?

假设主线程和worker正在不同套接字中的不同CPU上运行。我认为会发生的是,主线程从其CPU的缓存中读取thread_finished。当worker更新它时,缓存一致性协议负责将worker更改为全局内存并使主线程的CPU缓存无效,因此它必须从全局内存中读取更新的值。缓存一致性的全部意义不是让上面的代码正常工作吗?

4 个答案:

答案 0 :(得分:21)

  

对收到的答案发表评论的人声称,不能使用简单的bool变量作为信号,代码在没有内存屏障的情况下被破坏,并且使用std :: atomic将是正确的。

评论者是对的:简单的bool是不够的,因为可以重新排序将thread_finished设置为true的线程的非原子写入。

考虑一个线程,它将静态变量x设置为某个非常重要的数字,然后表示它的退出,如下所示:

x = 42;
thread_finished = true;

当主线程将thread_finished设置为true时,它会假定工作线程已完成。但是,当您的主线程检查x时,它可能会发现它设置为错误的数字,因为上面的两个写入已被重新排序。

当然,这只是一个简单的例子来说明一般问题。对std::atomic变量使用thread_finished会添加内存屏障,确保在完成之前完成所有写操作。这解决了无序写入的潜在问题。

另一个问题是对非易失性变量的读取可以在 中进行优化,因此主线程永远不会注意到{{1}中的更改标志。

<小时/> 重要说明:让thread_finished volatile 来解决问题;事实上,volatile不应与线程一起使用 - 它适用于内存映射硬件。

答案 1 :(得分:7)

使用原始bool是不够的。

  

程序的执行包含数据竞争,如果它在不同的线程中包含两个冲突的动作,其中至少有一个不是原子的,并且在另一个之前都不会发生。任何此类数据争用都会导致未定义的行为。 §1.10p21

     

如果其中一个修改内存位置(1.7)而另一个访问或修改相同的内存位置,则两个表达式评估会发生冲突。 §1.10p4

你的程序包含一个数据竞争,其中工作线程写入bool并且主线程从中读取,但是操作之间没有正式的发生之前的关系。

有许多不同的方法可以避免数据竞争,包括使用具有适当内存排序的std::atomic<bool>,使用内存屏障,或者用条件变量替换bool。

答案 2 :(得分:2)

不行。优化器可以优化

  while ( !thread_finished ) {
    // do some own work until the thread has finished ...
  }

为:

  if(!thread_finished)
    while (1) {
      // do some own work until the thread has finished ...
    }

假设它可以证明,“某些自己的工作”不会改变thread_finished

答案 3 :(得分:2)

高速缓存一致性算法并不是随处可见,也不是完美的。围绕thread_finished的问题是,一个线程尝试向其写入值,而另一个线程尝试读取它。这是一个数据竞争,如果访问没有排序,则会导致未定义的行为。