C++ std::atomic 在程序员级别有啥保证?

Posted

技术标签:

【中文标题】C++ std::atomic 在程序员级别有啥保证?【英文标题】:What is guaranteed with C++ std::atomic at the programmer level?C++ std::atomic 在程序员级别有什么保证? 【发布时间】:2020-05-16 21:26:27 【问题描述】:

我已经听过并阅读了几篇关于 std::atomic 的文章、演讲和 *** 问题,我想确定我已经很好地理解了它。因为由于 MESI(或派生的)缓存一致性协议、存储缓冲区、无效队列等可能存在延迟,我仍然对缓存行写入可见性感到有些困惑。

我读到 x86 具有更强的内存模型,如果缓存失效被延迟,x86 可以恢复开始的操作。但我现在只对作为 C++ 程序员的假设感兴趣,与平台无关。

[T1:thread1 T2:thread2 V1:共享原子变量]

我了解 std::atomic 保证,

(1) 变量上不会发生数据竞争(由于对缓存行的独占访问)。

(2) 根据我们使用的 memory_order,它保证(通过屏障)发生顺序一致性(在屏障之前、在屏障之后或两者兼而有之)。

(3) 在 T1 上的原子写入 (V1) 之后,T2 上的原子 RMW(V1) 将是一致的(其缓存行将使用 T1 上的写入值更新)。

但正如cache coherency primer 提到的,

所有这些事情的含义是,默认情况下,负载可以获取陈旧数据(如果相应的失效请求位于失效队列中)

那么,下面的说法正确吗?

(4) std::atomic 不保证 T2 在 T1 上的原子写入 (V) 后不会在原子读取 (V) 上读取“陈旧”值。

如果(4)是正确的问题:如果T1上的原子写入使缓存行失效,无论延迟如何,为什么T2在原子RMW操作而不是原子读取时等待失效有效?

如果 (4) 错误的问题:线程何时可以读取“陈旧”值并且在执行中“可见”?

非常感谢您的回答

更新 1

所以看来我在 (3) 上错了。想象以下交错,初始 V1=0:

T1: W(1)
T2:      R(0) M(++) W(1)

即使在这种情况下,T2 的 RMW 保证完全在 W(1) 之后发生,它仍然可以读取“陈旧”值(我错了)。据此,原子不保证完全缓存一致性,只保证顺序一致性。

更新 2

(5) 现在想象这个例子(x = y = 0 并且是原子的):

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");

根据我们所说的,除了 T2 是在 T1 之后执行之外,看到屏幕上显示的“msg”不会给我们提供信息。因此,可能发生了以下任一处决:

T1 T1

对吗?

(6) 如果一个线程总是可以读取“陈旧”的值,如果我们采用典型的“发布”场景,而不是发出一些数据准备就绪的信号,而是做相反的事情(删除数据),会发生什么?

T1: delete gameObjectPtr; is_enabled.store(false, std::memory_order_release);
T2: while (is_enabled.load(std::memory_order_acquire)) gameObjectPtr->doSomething();

在看到 is_enabled 为 false 之前,T2 仍将使用已删除的 ptr。

(7) 此外,线程可能读取“陈旧”值这一事实意味着 mutex 不能仅用一个无锁原子来实现,对吧?它需要线程之间的同步机制。它需要一个可锁定的原子吗?

【问题讨论】:

【参考方案1】:
    是的,没有数据争用 是的,使用适当的 memory_order 值可以保证顺序一致性 原子读取-修改-写入将始终完全在原子写入同一变量之前或之后发生 是的,在对 T1 进行原子写入后,T2 可以从变量中读取陈旧值

原子读-修改-写操作被指定为保证它们的原子性。如果另一个线程可以在初始读取之后和写入 RMW 操作之前写入该值,那么该操作将不是原子操作。

线程总是可以读取过时的值,除非发生之前保证相对排序

如果 RMW 操作读取“陈旧”值,则它保证它生成的写入将在来自其他线程的任何写入覆盖它读取的值之前可见。

例如更新

如果 T1 写入 x=1 而 T2 写入 x++x 最初为 0,则从 x 的存储角度来看,选择是:

    T1 先写,所以 T1 写 x=1,然后 T2 读 x==1,将其增加到 2 并作为单个原子操作写回 x=2

    T1 的写入排在第二位。 T2 读取 x==0,将其递增到 1,然后将 x=1 作为单个操作写回,然后 T1 写入 x=1

但是,如果这两个线程之间没有其他同步点,则线程可以继续执行未刷新到内存的操作。

因此,T1 可以发出x=1,然后继续执行其他操作,即使 T2 仍会读取 x==0(并因此写入 x=1)。

如果还有其他同步点,那么哪个线程首先修改x 就会变得很明显,因为这些同步点会强制执行顺序。

如果您对从 RMW 操作中读取的值设置条件,这一点最为明显。

更新 2

    如果您对所有原子操作使用memory_order_seq_cst(默认值),则无需担心这类事情。从程序的角度来看,如果您看到“msg”,则 T1 运行,然后是 T3,然后是 T2。

如果您使用其他内存排序(尤其是memory_order_relaxed),那么您可能会在代码中看到其他情况。

    在这种情况下,您有一个错误。假设is_enabled 标志为真,当T2 进入其while 循环时,它决定运行主体。 T1 现在删除数据,然后 T2 遵循指针,这是一个悬空指针,未定义的行为随之而来。除了防止旗帜上的数据竞争之外,原子不会以任何方式帮助或阻碍。

    可以使用单个原子变量实现互斥锁。

【讨论】:

非常感谢@Anthony Wiliams 的快速回答。我用 RMW 读取“陈旧”值的示例更新了我的问题。看看这个例子,你所说的相对排序是什么意思,T2 的 W(1) 在任何写入之前都是可见的?这是否意味着一旦 T2 看到了 T1 的变化,它就不会再读取 T2 的 W(1) 了? 因此,如果“线程始终可以读取陈旧值”,则意味着永远无法保证缓存的一致性(至少在 c++ 程序员级别)。你能看看我的update2吗? 现在我发现我应该更多地关注语言和硬件内存模型以完全理解所有这些,这就是我缺少的部分。非常感谢!【参考方案2】:

关于 (3) - 这取决于使用的内存顺序。如果存储和 RMW 操作都使用std::memory_order_seq_cst,那么这两个操作都以某种方式排序 - 即存储发生在 RMW 之前,或者相反。如果存储在 RMW 之前是 order,则保证 RMW 操作“看到”存储的值。如果 store 在 RMW 之后排序,则会覆盖 RMW 操作写入的值。

如果您使用更宽松的内存顺序,修改仍将以某种方式排序(变量的修改顺序),但您无法保证 RMW 是否“看到”来自存储操作的值 - 即使RMW 操作是在变量的修改顺序中写入之后

如果您还想阅读另一篇文章,我可以将您推荐给Memory Models for C/C++ Programmers。

【讨论】:

感谢您的文章,我还没有读过它。即使它已经很老了,将我的想法放在一起也很有用。 很高兴听到这个消息 - 这篇文章是我硕士论文的一个略微扩展和修改的章节。 :-) 它侧重于介绍 C++11 的内存模型;我可能会更新它以反映 C++14/17 中引入的(小)变化。如果您有任何改进或改进建议,请告诉我!

以上是关于C++ std::atomic 在程序员级别有啥保证?的主要内容,如果未能解决你的问题,请参考以下文章

C++ error: use of deleted function ‘std::atomic<short unsigned int>::atomic(const std::atomic<short

std :: atomic的正确用法

C++ 原子操作 std::atomic<int>

C++并发与多线程 11_std::atomic叙谈std::launch(std::async) 深入

在实践中,C++11 中 std::atomic 的内存占用是多少?

如何用 std::atomic 实现无锁计数器?