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

Posted

技术标签:

【中文标题】是否有必要使用 std::atomic 来表示线程已完成执行?【英文标题】:Is it necessary to use a std::atomic to signal that a thread has finished execution? 【发布时间】:2012-12-31 04:42:22 【问题描述】:

我想检查 std::thread 是否已完成执行。搜索 *** 我发现以下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&lt;bool&gt; 是正确的。我最初的猜测是这是错误的,一个简单的bool 就足够了,但我想确保我没有遗漏任何东西。 上述代码是否需要std::atomic&lt;bool&gt; 才能正确?

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

【问题讨论】:

为什么不使用条件变量或信号量或 autoresetevent 来向线程发出信号?这就是这些东西的用途。 如果编译器根据您一遍又一遍地测试变量的值并最终修改应用程序的行为这一事实进行了一些优化,则可能会出现问题。我从未见过这种情况发生,但我听说这可能是使用原子而不是简单布尔值的原因。 @TonyTheLion:条件变量、信号量和事件用于您想要等待(暂停线程)直到发生某些事情。他只是想测试是否发生了某些事情,因此原子布尔更合适。 相关问题:***.com/q/12507705/819272 另请参阅此答案的 cmets:***.com/a/12087141/819272 【参考方案1】:

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

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

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

x = 42;
thread_finished = true;

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

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

另一个问题是可以优化对非易失性变量的读取,因此主线程永远不会注意到 thread_finished 标志的变化。


重要提示:让您的thread_finished 易变不会解决问题;事实上,volatile 不应该与线程一起使用——它旨在与内存映射硬件一起使用。

【讨论】:

+1 用于提及易失性和内存映射硬件。没有足够的人理解这一点:) volatile 将阻止 bool 被优化,但它不会提供 membar,或保证任何时间框架来检查缓存与主内存的一致性。 atomic bool 是正确的选择。 “确保所有写入之前都完成”听起来像是x = 42; membar; thread_finished = true; x = 42; thread_finished = true; membar;的情况——前者不能确保及时thread_finished 更新的可见性,后者可能会冒着在 membar 启动之前重新排序曝光的风险。membar 实际上可能是每个 x = 42; membar(thread_finished = true); 的前缀/修饰符,确保相对的“ 和 两个更新的及时可见性,或者每个x = 42; membar; thread_finished = true; membar 需要使用两次。干杯。【参考方案2】:

使用原始的bool 是不够的。

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

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

您的程序包含数据竞争,其中工作线程写入 bool,主线程从中读取,但操作之间没有正式的 happens-before 关系。

有许多不同的方法可以避免数据争用,包括使用 std::atomic&lt;bool&gt; 和适当的内存排序、使用内存屏障或用条件变量替换布尔值。

【讨论】:

【参考方案3】:

这不好。优化器可以优化

  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

【讨论】:

【参考方案4】:

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

【讨论】:

以上是关于是否有必要使用 std::atomic 来表示线程已完成执行?的主要内容,如果未能解决你的问题,请参考以下文章

使用原子的线程安全单例

std::atomic_flag 停止多个线程

使用 std::atomic<> 进行多线程

如何用 std::atomic 实现无锁计数器?

std::atomic_flag 初始化结果

是否可以使用 std::atomic 使类原子的复杂成员函数?