在程序员级别使用 C++ std::atomic 保证什么?

Alb*_*das 9 c++ concurrency caching atomic c++17

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

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

[T1:线程1 T2:线程2 V1:共享原子变量]

我知道 std::atomic 保证,

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

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

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

但正如缓存一致性入门所提到的,

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

那么,以下说法正确吗?

(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)
Run Code Online (Sandbox Code Playgroud)

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

更新 2

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

T1: x = 1;
T2: y = 1;
T3: if (x==1 && y==0) print("msg");
Run Code Online (Sandbox Code Playgroud)

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

  • T1 < T3 < T2
  • T1 < T2 < T3(其中 T3 看到 x = 1 但还没有看到 y = 1)

那正确吗?

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

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

其中 T2 仍将使用已删除的 ptr,直到看到 is_enabled 为 false。

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

Ant*_*ams 4

  1. 是的,没有数据竞争
  2. 是的,使用适当的memory_order值可以保证顺序一致性
  3. 原子读取-修改-写入始终发生在对同一变量进行原子写入之前或之后
  4. 是的,在 T1 上进行原子写入后,T2 可以从变量中读取过时的值

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

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

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

更新例如

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

  1. T1 的写入是第一个,因此 T1 写入x=1,然后 T2 读取x==1,将其增加到 2 并x=2作为单个原子操作写回。

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

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

因此,T1 可以发出x=1,然后继续处理其他事情,即使 T2 仍然会读取x==0(并因此写入x=1)。

如果存在任何其他同步点,那么哪个线程x首先修改就会变得显而易见,因为这些同步点将强制执行顺序。

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

更新2

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

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

  1. 在这种情况下,你有一个错误。假设is_enabled标志为 true,当 T2 进入while循环时,它决定运行主体。T1 现在删除数据,然后 T2 引用指针(这是一个悬空指针),并随之发生未定义的行为。除了防止标志上的数据竞争之外,原子不会以任何方式帮助或阻碍。

  2. 可以使用单个原子变量来实现互斥体。