让我们看一下这个结构:
struct entry {
atomic<bool> valid;
atomic_flag writing;
char payload[128];
}
两个踏板 A 和 B 以这种方式同时访问这个结构(假设e
是 的一个实例entry
):
if (e.valid) {
// do something with e.payload...
} else {
while (e.writing.test_and_set(std::memory_order_acquire));
if (!e.valid) {
// write e.payload one byte at a time
// (the payload written by A may be different from the payload written by B)
e.valid = true;
e.writing.clear(std::memory_order_release);
}
}
我猜这段代码是正确的并且不会出现问题,但我想了解它为什么会起作用。
引用 C++ 标准(29.3.13):
实现应该使原子存储在合理的时间内对原子负载可见。
现在,记住这一点,想象线程 A 和 B 都进入了else
块。这种交错可能吗?
- 两者都
A
进入B
分支else
,因为valid
是false
A
设置writing
标志B
开始在writing
标志上旋转锁定A
读取valid
标志(即false
)并进入if
块A
写入有效载荷A
写入true
有效标志;显然,如果再次A
阅读valid
,它将阅读true
A
清除writing
标志B
设置writing
标志B
读取有效标志 ( ) 的陈旧值false
并进入if
块B
写入其有效载荷B
写true
在valid
国旗上B
清除writing
标志
我希望这是不可能的,但是当谈到实际回答“为什么不可能?”这个问题时,我不确定答案。这是我的想法。
再次引用标准(29.3.12):
原子读-修改-写操作应始终读取在与读-修改-写操作关联的写之前写入的最后一个值(按修改顺序)。
atomic_flag::test_and_set()
是一个原子读-修改-写操作,如 29.7.5 中所述。
由于atomic_flag::test_and_set()
总是读取“新值”,并且我正在使用std::memory_order_acquire
内存排序调用它,因此我无法读取标志的陈旧值,因为我必须看到调用之前valid
引起的所有副作用(使用)。A
atomic_flag::clear()
std::memory_order_release
我对么?
澄清。我的整个推理(错误或正确)依赖于 29.3.12。就我目前的理解而言,如果我们忽略atomic_flag
,valid
即使它是atomic
. atomic
似乎并不意味着每个线程“总是立即可见”。您可以要求的最大保证是您读取的值的顺序一致,但您仍然可以在获取新数据之前读取陈旧数据。幸运的是,atomic_flag::test_and_set()
每个exchange
操作都有这个关键特性:它们总是读取新数据。因此,只有当您在writing
标志上获取/释放时(不仅在 上valid
),您才会得到预期的行为。你明白我的观点(正确与否)吗?
编辑:我最初的问题包括以下几行,如果与问题的核心相比,这些行引起了太多关注。我留下它们是为了与已经给出的答案保持一致,但如果你现在正在阅读这个问题,请忽略它们。
valid
成为 anatomic<bool>
而不是 plainbool
atomic<bool>
有什么意义吗?此外,如果它应该是,那么它不会出现问题的“最小”内存排序约束是什么?