【发布时间】:2020-02-22 17:39:27
【问题描述】:
我编写了一个基本的图形调度程序,它以无等待的方式同步任务执行。由于图拓扑是不可变的,我想我会让所有原子操作放松。然而,随着我对 CPU 硬件的了解越来越多,我开始担心我的数据结构在内存模型较弱的平台上的行为(我只在 x86 上测试过我的代码)。这是困扰我的场景:
线程 1 (T1) 和线程 2 (T2) 分别同时更新(非原子)内存位置 X 和 Y,然后继续执行其他不相关的任务。
线程 3 (T3) 在 T1 和 T2 完成后拾取一个依赖任务,加载 X 和 Y,并将它们相加。没有获取/释放同步、线程连接或调用锁,并且 T3 的任务保证在 T1 和 T2 完成后调度。
假设 T1、T2 和 T3 被调度(由操作系统)在不同的 CPU 内核上,我的问题是:在没有任何内存栅栏或类似锁的指令的情况下,T3 是否保证看到最新的X 和 Y 的值? 另一种问法是:如果不插入栅栏,存储后多久可以执行加载,或者对此没有任何保证?强>
我担心无法保证执行 T1 和 T2 的内核在 T3 的内核尝试加载该信息时已刷新其存储缓冲区。我倾向于将数据竞争视为由于同时发生的加载和存储(或存储和存储)而发生的数据损坏。但是,我开始意识到,考虑到微尺度 CPU 的分布式特性,我不太确定 同时 的真正含义。根据 CppRef:
具有两个冲突评估的程序存在数据竞争,除非:
- 两个评估都在同一个线程或同一个信号处理程序中执行,或者
- 两个冲突的计算都是原子操作(参见 std::atomic),或者
- 其中一个冲突的评估发生在另一个之前(参见 std::memory_order)
这似乎暗示任何使用我的图形调度程序的人都会遇到数据竞争(假设他们自己不保护它),即使我可以保证 T3 在 T1 和 T2 完成之前不会执行。我还没有在我的测试中观察到数据竞争,但我还没有天真地认为仅靠测试就足以证明这一点。
【问题讨论】:
-
我对 C++ 内存模型并不十分熟悉,但我想如果没有围栏,T3 可以很容易地看到缓存中的陈旧数据。如果您不进行任何线程连接或以其他方式进行同步,那么将 T3 安排在 T1 和 T2 之后就相当于君子协定。
-
存储后多久可以执行加载, ISO C++ 对时间进行零保证。当您需要的只是在调度程序本身的某处获取/释放同步时,依靠时间/距离来确保正确性几乎总是一个坏主意,例如T1 和 T2 使用发布存储声明自己完成。否则 T3 在 T1 和 T2 之后执行意味着什么?
-
(是的,在 x86 上进行测试并不能证明任何事情;硬件内存模型基本上是 acq_rel,因此编译时重新排序会选择一些合法的顺序,然后该顺序使用 acq_rel 运行。)
-
@PeterCordes 干杯,彼得!我想在添加任何额外的同步之前我会先问一下,因为我对非 SC 原子还是很陌生,并且认为太多是理所当然的。
标签: c++ multithreading atomic memory-barriers memory-model