松弛的内存顺序会导致这里无限循环吗?
Will relaxed memory order lead to infinite loop here?
有问题的代码:
#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
,我假设编译器可以自由地在 t.join()
之后重新排序 stop.store()
。结果,t.join()
永远不会 return。这个推理正确吗?
如果是,将 stop.store(true, std::memory_order_relaxed)
更改为 stop.store(true)
是否可以解决问题?
[intro.progress]/18:
An implementation should ensure that the last value (in modification
order) assigned by an atomic or synchronization operation will become
visible to all other threads in a finite period of time.
[atomics.order]/12:
Implementations should make atomic stores visible to atomic loads
within a reasonable amount of time.
这是一个不具约束力的建议。如果您的实施遵循它们——高质量的实施应该如此——你就没问题。否则,你就完蛋了。在这两种情况下,无论使用的内存顺序如何。
C++抽象机没有"reordering"的概念。在抽象语义中,主线程存储到原子然后阻塞,所以如果实现使存储在有限时间内对加载可见,那么另一个线程将在有限时间内加载这个存储的值并且终止。相反,如果实现出于某种原因没有这样做,那么您的其他线程将永远循环。使用的内存顺序无关紧要。
我从未发现关于 "reordering" 的推理有用。它将低级实现细节与高级内存模型混合在一起,往往会使事情变得更加混乱,而不是更少。
任何其定义在当前翻译单元中不可用的函数都被视为 I/O 函数。假定此类调用会导致副作用,并且编译器无法将后续语句移到调用之前或将前面的语句移到调用之后。
Reading an object designated by a volatile glvalue ([basic.lval]), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression (or a subexpression) in general includes both value computations (including determining the identity of an object for glvalue evaluation and fetching a value previously assigned to an object for prvalue evaluation) and initiation of side effects. When a call to a library I/O function returns or an access through a volatile glvalue is evaluated the side effect is considered complete, even though some external actions implied by the call (such as the I/O itself) or by the volatile access may not have completed yet.
和
Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated.
这里的std::thread
构造函数和std::thread::join
就是这样的有副作用的函数(它们最终会调用当前TU中不可用的平台特定线程函数)。 stop.store
也会引起副作用(内存存储是一种副作用)。因此 stop.store
不能移动到 std::thread
构造函数之前或过去的 std::thread::join
调用。
有问题的代码:
#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
,我假设编译器可以自由地在 t.join()
之后重新排序 stop.store()
。结果,t.join()
永远不会 return。这个推理正确吗?
如果是,将 stop.store(true, std::memory_order_relaxed)
更改为 stop.store(true)
是否可以解决问题?
[intro.progress]/18:
An implementation should ensure that the last value (in modification order) assigned by an atomic or synchronization operation will become visible to all other threads in a finite period of time.
[atomics.order]/12:
Implementations should make atomic stores visible to atomic loads within a reasonable amount of time.
这是一个不具约束力的建议。如果您的实施遵循它们——高质量的实施应该如此——你就没问题。否则,你就完蛋了。在这两种情况下,无论使用的内存顺序如何。
C++抽象机没有"reordering"的概念。在抽象语义中,主线程存储到原子然后阻塞,所以如果实现使存储在有限时间内对加载可见,那么另一个线程将在有限时间内加载这个存储的值并且终止。相反,如果实现出于某种原因没有这样做,那么您的其他线程将永远循环。使用的内存顺序无关紧要。
我从未发现关于 "reordering" 的推理有用。它将低级实现细节与高级内存模型混合在一起,往往会使事情变得更加混乱,而不是更少。
任何其定义在当前翻译单元中不可用的函数都被视为 I/O 函数。假定此类调用会导致副作用,并且编译器无法将后续语句移到调用之前或将前面的语句移到调用之后。
Reading an object designated by a volatile glvalue ([basic.lval]), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression (or a subexpression) in general includes both value computations (including determining the identity of an object for glvalue evaluation and fetching a value previously assigned to an object for prvalue evaluation) and initiation of side effects. When a call to a library I/O function returns or an access through a volatile glvalue is evaluated the side effect is considered complete, even though some external actions implied by the call (such as the I/O itself) or by the volatile access may not have completed yet.
和
Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated.
这里的std::thread
构造函数和std::thread::join
就是这样的有副作用的函数(它们最终会调用当前TU中不可用的平台特定线程函数)。 stop.store
也会引起副作用(内存存储是一种副作用)。因此 stop.store
不能移动到 std::thread
构造函数之前或过去的 std::thread::join
调用。