宽松的记忆顺序会导致无限循环吗?

时间:2018-05-22 07:05:30

标签: c++ c++11 concurrency atomic memory-model

有问题的代码:

#include <atomic>
#include <thread>

std::atomic_bool stop(false);

void wait_on_stop() {
  while (!stop.load(std::memory_order_relaxed));
}

int main() {
  std::thread t(wait_on_stop);
  stop.store(true, std::memory_order_relaxed);
  t.join();
}

由于此处使用std::memory_order_relaxed,我假设编译器可以在stop.store()之后重新排序t.join()。结果,t.join()将永远不会返回。这种推理是否正确?

如果是,是否会将stop.store(true, std::memory_order_relaxed)更改为stop.store(true)来解决问题?

2 个答案:

答案 0 :(得分:3)

[intro.progress] / 18:

  

实现应该确保最后一个值(在修改中   由原子或同步操作分配的命令)将成为   在有限的时间内对所有其他线程可见。

[atomics.order] / 12:

  

实现应该使原子库对原子载荷可见   在合理的时间内。

这是一项非约束性建议。如果你的实现遵循它们 - 作为高质量的实现应该 - 你很好。否则,你被搞砸了。在这两种情况下,无论使用何种内存顺序。

C ++抽象机器没有“重新排序”的概念。在抽象语义中,主线程存储在原子中然后被阻塞,因此如果实现使存储在有限的时间内对加载可见,那么另一个线程将在有限的时间内加载该存储的值并且终止。相反,如果实现由于某种原因不这样做,那么你的另一个线程将永远循环。使用的内存顺序无关紧要。

我从来没有找到关于“重新排序”有用的推理。它将低级实现细节与高级内存模型混合在一起,往往会让事情变得更加混乱,而不是更少。

答案 1 :(得分:1)

当前翻译单元中定义不可用的任何功能都被视为I / O功能。假设这样的调用会引起副作用,并且编译器不能将以下语句移动到调用之前或前面的语句之后跟随调用。

[intro.execution]

  

读取由volatile glvalue([basic.lval])指定的对象,修改对象,调用库I / O函数或调用执行任何这些操作的函数都是副作用,这些都是执行环境的状态。表达式(或子表达式)的评估通常包括值计算(包括确定用于glvalue评估的对象的身份以及获取先前分配给用于prvalue评估的对象的值)和启动副作用。当对库I / O函数的调用返回或通过volatile glvalue进行访问时,即使调用所隐含的某些外部操作(例如I / O本身)或易失性访问,也会认为副作用已完成可能尚未完成。

并且

  

在每个值计算和与要评估的下一个完整表达式相关的副作用之前,对与全表达式相关的每个值计算和副作用进行排序。

这里std::thread构造函数和std::thread::join是这样的函数(它们最终调用当前TU中不可用的平台特定线程函数)并带有副作用。 stop.store也会导致副作用(内存存储是副作用)。因此,在stop.store构造函数或过去的std::thread调用之前无法移动std::thread::join