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


9

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

我读到x86具有更强的内存模型,并且如果缓存失效被延迟,x86可以还原启动的操作。但是我现在只对我作为独立于平台的C ++程序员应该承担的兴趣感兴趣。

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

我了解std :: atomic可以保证,

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

(2)取决于我们使用哪种memory_order,它(在有障碍的情况下)保证发生顺序一致性(在障碍之前,之后或之后)。

(3)在T1上执行原子write(V1)之后,T2上的原子RMW(V1)将是连贯的(其缓存行将已用T1上的写入值进行更新)。

但是正如缓存一致性入门所述

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

那么,以下正确吗?

(4)std::atomic不保证T2在T1上执行原子write(V)之后不会读取原子read(V)上的“陈旧”值。

问题(4)是否正确:如果无论延迟如何,在T1上进行原子写入都会使高速缓存行无效,那么当原子RMW操作而不是在原子读取上进行操作时,T2为什么要等待无效生效?

问题(4)是否错误:线程何时可以在执行过程中读取“过时”值并且“可见”?

非常感谢您的回答

更新1

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

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

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

更新2

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

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

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

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

那正确吗?

(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)另外,线程可能会读取“过时”的值,这意味着不能仅使用一个无锁原子权限来实现互斥锁吗?这将需要线程之间的同步机制。是否需要可锁定的原子?

Answers:


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

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

线程始终可以读取过时的值,除非在保证相对排序之前发生。

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

例如更新

如果T1写入x=1而T2确实x++x0开头,则从存储的角度来看的选择x是:

  1. 首先是T1的写操作,所以T1进行写操作x=1,然后T2进行读取x==1,将其递增为2,然后x=2作为单个原子操作写回。

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

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

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

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

如果您对从RMW操作读取的值有条件,这将是最明显的。

更新2

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

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

  1. 在这种情况下,您有一个错误。假设该is_enabled标志为true,当T2进入其while循环时,它决定运行主体。现在,T1删除数据,然后T2引用该指针,该指针是一个悬空的指针,并且随之发生未定义的行为。原子除了阻止标志上的数据争用外,无济于事。

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


非常感谢@Anthony Wiliams的快速回答。我以RMW读取“陈旧”值的示例更新了我的问题。看这个例子,您说相对顺序是什么意思,并且在任何写操作之前T2的W(1)是可见的?这是否意味着一旦T2看到T1的更改,就不再读取T2的W(1)了吗?
艾伯特·卡尔达斯

因此,如果“线程总是可以读取陈旧的值”,则意味着永远不能保证缓存的一致性(至少在c ++程序员级别)。你能看一下我的update2吗?
艾伯特·卡尔达斯

现在,我发现我应该更加关注语言和硬件内存模型,以完全理解所有这些,而这正是我所缺少的。非常感谢!
艾伯特·卡尔达斯

1

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

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

如果您想阅读另一篇文章,我可以推荐您使用C / C ++程序员的内存模型


感谢您的文章,我还没有读过。即使它已经很老了,将我的想法整合在一起也很有用。
Albert Caldas

1
很高兴听到这个消息-本文是我硕士论文中稍作扩展和修订的章节。:-)着重介绍了C ++ 11引入的内存模型;我可能会对其进行更新,以反映C ++ 14/17中引入的(小的)更改。如果您有任何意见或建议,请与我联系!
mpoeter
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.