繁体   English   中英

C ++ 11 atomic:为什么这段代码有效?

[英]C++11 atomic: why does this code work?

我们来看看这个结构:

struct entry {
    atomic<bool> valid;
    atomic_flag writing;
    char payload[128];
}

两个步骤A和B以这种方式同时访问此结构(让eentry实例):

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. AB进入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. Bvalid标志上写入true
  12. B清除writing标志

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

再次引用标准(29.3.12):

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

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

由于atomic_flag::test_and_set()总是读取一个“新值”,并且我用std::memory_order_acquire内存排序调用它, 然后我无法读取valid标志的陈旧值,因为我必须看到所有的一面 -在atomic_flag::clear()调用之前由A引起的效果(使用std::memory_order_release )。

我对么?

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


编辑 :我的原始问题包括以下几行,如果与问题的核心相比得到了太多关注。 我让他们与已经给出的答案保持一致,但如果你现在正在阅读这个问题,请忽略它们。

validatomic<bool> 不是普通的bool 是否有任何意义 而且,如果它应该是一个atomic<bool> 那么它的“最小”内存排序约束是 什么?

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

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

除此之外,您的分析似乎有些合理,但对我来说并不完全正确。 记忆顺序要记住的事情是获取和释放语义将配对。 在对同一版本的获取操作读取修改值之后,可以安全地读取在释放操作之前写入的所有内容。 考虑到这一点, waiting.clear(...)上的发布语义确保当writing.test_and_set(...)上的循环退出时,写入valid必须是可见的,因为后者读取等待的更改(the write done in waiting.clear(...)`)中使用获取语义完成,并且在该更改可见之前不退出。

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

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

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

第29.3.12节与此代码正确或不正确的原因无关。 您想要的部分( 在线可用标准的草案版本 )是第1.10节:“多线程执行和数据竞争”。 第1.10节定义了原子操作和原子操作非原子操作的先发生关系。

第1.10节说如果有两个非原子操作,你无法确定之前发生的关系,那么你就有了数据竞争。 它进一步声明(第21段)任何具有数据争用的程序都有未定义的行为。

如果e.valid不是原子的,那么您在第一行代码和行e.valid=true之间会有数据竞争。 因此,关于else子句中的行为的所有推理都是不正确的(程序没有定义的行为,所以没有任何理由可以解释。)

在另一方面,如果所有的访问,以e.valid是由原子操作上保护e.writing (比如如果else条款是你的整个程序),那么你的推理是正确的。 列表中的事件9不可能发生。 但原因不是第29.3.12节,而是第1.10节,它表示如果没有数据条,你的非原子操作似乎会顺序一致。

您正在使用的模式被称为双重检查锁定 在C ++ 11之前,无法实现双重检查锁定。 在C ++ 11中,您可以正确且可移植地进行双重检查锁定。 你这样做的方式是声明validatomic

如果valid不是原子那么的初始读取e.valid与分配的第一行冲突e.valid

在其中一个线程获得自旋锁之前,无法保证两个线程都已经完成了读取,即步骤1和6未被排序。

e.valid的商店需要发布,条件中的负载需要获取。 否则,编译器/处理器可以自由地在写入有效载荷之前设置e.valid。 有一个开源工具CDSChecker,用于根据C / C ++ 11内存模型验证这样的代码。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM