在没有同步的情况下松散的原子和内存一致性

Relaxed Atomics and Memory Coherence in the Absence of Synchronisation

我编写了一个基本的图形调度程序,它以无等待的方式同步任务执行。由于图形拓扑是不可变的,我想我会让所有原子操作放松。然而,随着我对 CPU 硬件的了解越来越多,我开始担心我的数据结构在内存模型较弱的平台上的行为(我只在 x86 上测试过我的代码)。这是困扰我的场景:

线程 1 (T1) 和线程 2 (T2) 分别同时更新(非原子地)内存位置 X 和 Y,然后继续执行其他不相关的任务。

线程 3 (T3) 在 T1 和 T2 完成后选择一个依赖任务,加载 X 和 Y,并将它们相加。没有 acquire/release 同步、线程连接或调用锁,T3 的任务保证在 T1 和 T2 完成后安排。

假设 T1、T2 和 T3 被调度(由 OS)在不同的 CPU 核心上,我的问题是:在没有任何内存栅栏或类似锁的指令,T3 是否保证看到 X 和 Y 的最新值? 另一种提问方式是:如果不插入栅栏,存储后多久可以您执行负载,还是对此没有任何保证?

我担心的是 无法保证在 T3 的核心尝试加载该信息时执行 T1 和 T2 的核心已经刷新了它们的存储缓冲区。我倾向于将数据竞争视为由于加载和存储(或存储和存储)同时发生而发生的数据损坏。但是,我开始意识到,考虑到 CPUs 在微观尺度上的分布式特性,我不太确定 同时 的真正含义。根据 CppRef:

A program that has two conflicting evaluations has a data race unless:

  • both evaluations execute on the same thread or in the same signal handler, or
  • both conflicting evaluations are atomic operations (see std::atomic), or
  • one of the conflicting evaluations happens-before another (see std::memory_order)

这似乎意味着任何使用我的图形调度程序的人都会遇到数据竞争(假设他们自己不防止这种情况),即使我可以保证 T3 在 T1 和 T2 完成之前不会执行。我还没有在我的测试中观察到数据竞争,但我还没有天真到认为仅靠测试就足以证明这一点。

how long after a store can you perform a load

ISO C++ 对时序做出零保证。依靠时间/距离来保证正确性几乎总是一个坏主意。

在这种情况下,您只需要 acquire/release 在调度程序本身的某处进行同步,例如T1 和 T2 使用发布存储声明自己已完成,并且调度程序使用获取负载检查它。

否则,T3 在 T1 和 T2 之后执行是什么意思? 如果调度程序可以尽早看到 "I'm done" 存储,它可以在 T1 启动时启动 T3或 T2 未完成其所有商店。

如果您确保 T3 中的一切都发生 T1 和 T2 之后(使用获取负载 "synchronize-with" 来自每个 T1 的发布存储和 T2),你甚至不需要在 T1 和 T2 中使用原子,只需要在调度器机制中。

与 seq_cst 相比,获取加载和释放存储相对便宜。在真正的 HW 上,seq_cst 必须在存储后刷新存储缓冲区,而 release 则不需要。 x86 免费 acq_rel。


(是的,在 x86 上测试并不能证明任何事情;硬件内存模型基本上是 acq_rel,因此编译时重新排序会选择一些合法的顺序,然后该顺序以 acq_rel 运行.)


我不确定启动 new 线程是否能保证该线程中的所有内容 "happens after" 都指向该线程中。如果是这样,那么这在形式上是安全的。

如果不是,那么理论上 IRIW 重新排序是值得担心的事情。 (所有使用 seq_cst 负载的线程必须就 seq_cst 存储的全局顺序达成一致,但不能使用较弱的内存顺序。实际上,PowerPC 是现实生活中可以做到这一点的硬件,AFAIK,而且只有简而言之 windows. . 任何 std::thread 构造函数都将涉及系统调用并且在实践中足够长,并且无论如何都涉及障碍,无论 ISO C++ 是否正式保证。

如果您不是开始一个新线程,而是存储一个标记供工作人员查看,那么acq/rel又足够了; happens-before is transitive 所以 A -> B 和 B -> C 意味着 A -> C。