5

让我们看一下这个结构:

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块。这种交错可能吗?

  1. 两者都A进入B分支else,因为validfalse
  2. A设置writing标志
  3. B开始在writing标志上旋转锁定
  4. A读取valid标志(即false)并进入if
  5. A写入有效载荷
  6. A写入true有效标志;显然,如果再次A阅读valid,它将阅读true
  7. A清除writing标志
  8. B设置writing标志
  9. B读取有效标志 ( ) 的陈旧值false并进入if
  10. B写入其有效载荷
  11. Btruevalid国旗上
  12. B清除writing标志

我希望这是不可能的,但是当谈到实际回答“为什么不可能?”这个问题时,我不确定答案。这是我的想法。

再次引用标准(29.3.12):

原子读-修改-写操作应始终读取在与读-修改-写操作关联的写之前写入的最后一个值(按修改顺序)。

atomic_flag::test_and_set()是一个原子读-修改-写操作,如 29.7.5 中所述。

由于atomic_flag::test_and_set()总是读取“新值”,并且我正在使用std::memory_order_acquire内存排序调用它,因此我无法读取标志的陈旧值,因为我必须看到调用之前valid引起的所有副作用(使用)。Aatomic_flag::clear()std::memory_order_release

我对么?

澄清。我的整个推理(错误或正确)依赖于 29.3.12。就我目前的理解而言,如果我们忽略atomic_flagvalid即使它是atomic. atomic似乎并不意味着每个线程“总是立即可见”。您可以要求的最大保证是您读取的值的顺序一致,但您仍然可以在获取新数据之前读取陈旧数据。幸运的是,atomic_flag::test_and_set()每个exchange操作都有这个关键特性:它们总是读取新数据。因此,只有当您在writing标志上获取/释放时(不仅在 上valid),您才会得到预期的行为。你明白我的观点(正确与否)吗?


编辑:我最初的问题包括以下几行,如果与问题的核心相比,这些行引起了太多关注。我留下它们是为了与已经给出的答案保持一致,但如果你现在正在阅读这个问题,请忽略它们。

valid成为 anatomic<bool> 不是 plainboolatomic<bool> 有什么意义吗?此外,如果它应该是那么它不会出现 问题的“最小”内存排序约束是什么?

4

4 回答 4

5

else分支内部valid应该受到操作施加的获取/释放语义的保护waiting。然而,这并没有消除制作valid原子的需要:

您忘记在分析中包含第一行 ( if (e.valid))。如果valid是一个bool而不是atomic<bool>这个访问将是完全不受保护的。因此,您可能会遇到在完全写入/可见valid之前更改对其他线程可见的情况。payload这意味着一个线程可以在尚未完全写入时B评估并进入分支。e.validtruedo something with e.payloadpayload

除此之外,您的分析似乎有些合理,但对我来说并不完全正确。内存排序要记住的是,获取和释放语义将成对出现。在对同一个验证对象的获取操作读取修改后的值之后,可以安全地读取在发布操作之前写入的所有内容。考虑到这一点,释放语义waiting.clear(...)确保valid当循环退出时写入必须可见writing.test_and_set(...),因为后者读取(the write done in具有获取语义的waiting waiting.clear(...)`)的更改并且在此之前不会退出变化是可见的。

关于 §29.3.12:它与您的代码的正确性有关,但与读取陈旧valid标志无关。您不能在清除之前设置标志,因此获取释放语义将确保那里的正确性。§29.3.12 保护您免受以下情况的影响:

  1. A和B都进入else分支,因为valid为false
  2. A设置写标志
  3. B 看到一个陈旧的写入值并设置它
  4. A 和 B 都读取有效标志(为假),进入 if 块并写入有效负载,创建竞争条件

编辑:对于最小的订购约束:为商店的加载和发布获取可能应该完成这项工作,但是根据您的目标硬件,您最好保持顺序一致性。对于这些语义之间的区别,请看这里

于 2013-06-04T18:12:18.307 回答
2
于 2013-06-04T19:49:34.110 回答
1

如果valid不是原子的e.valid,则第一行的初始读取与对 的赋值冲突e.valid

不能保证两个线程在其中一个获得自旋锁之前已经完成了读取,即步骤 1 和 6 没有排序。

于 2013-06-04T18:19:41.317 回答
1

存储到 e.valid 需要释放,并且条件中的负载需要获取。否则,编译器/处理器可以自由设置 e.valid 高于写入有效负载。有一个开源工具 CDSChecker,用于针对 C/C++11 内存模型验证此类代码。

于 2013-09-02T05:00:10.367 回答