【问题标题】:Race condition on x86x86 上的竞争条件
【发布时间】:2011-07-08 11:12:24
【问题描述】:

谁能解释一下这个说法:

shared variables
x = 0, y = 0

Core 1       Core 2
x = 1;       y = 1;
r1 = y;      r2 = x;

如何在 x86 处理器上拥有 r1 == 0r2 == 0

来源"The Language of Concurrency" by Bartosz Milewski

【问题讨论】:

  • preshing.com/20120515/memory-reordering-caught-in-the-act 使用带有/不带有 mfence 的此示例的汇编版本来演示内存重新排序,并解释为什么允许它发生。在 C/C++ 中,如果它们不是 atomic,它只是普通的 UB;如果它们正在重新排序,则只能在 memory_order_release 或更弱的情况下发生。
  • 不幸的是(?)memory-order标签是memory-barriers的同义词,所以这个关于内存排序的问题只能用一个可以防止重新排序的标签来标记。 (至少对于足够强大的内存屏障......)

标签: c++ c x86 race-condition memory-barriers


【解决方案1】:

由于涉及reordering of instructions 的优化,可能会出现此问题。换句话说,两个处理器都可以在分配变量xy 之前分配r1r2,如果他们发现这会产生更好的性能。这可以通过添加memory barrier 来解决,这将强制执行排序约束。

引用您在帖子中提到的slideshow

现代多核/语言打破了顺序一致性

关于 x86 架构,最好的阅读资源是Intel® 64 and IA-32 Architectures Software Developer’s Manual(第 8.2 章内存排序)。 8.2.1 和 8.2.2 节描述了由 Intel486, Pentium, Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium 4, Intel Xeon 和 P6 系列处理器:一种称为处理器排序的内存模型,与旧 Intel386 架构的程序排序strong ordering)相反(其中读写指令总是按照它们在指令流中出现的顺序发出)。

手册描述了处理器排序内存模型的许多排序保证(例如 Loads 不会与其他负载重新排序Stores 不会与其他存储重新排序商店不会用旧负载重新排序等),但它还描述了允许的重新排序规则,这会导致 OP 帖子中的竞争条件:

8.2.3.4 加载可能会与之前的存储重新排序到不同的存储 地点

另一方面,如果指令的原始顺序被切换:

shared variables
x = 0, y = 0

Core 1       Core 2
r1 = y;      r2 = x;
x = 1;       y = 1;

在这种情况下,处理器保证r1 = 1r2 = 1 的情况是不允许的(由于8.2.3.3 Stores Are Not Reordered With Early Load保证),这意味着这些指令永远不会在单个内核中重新排序。

要将其与不同的架构进行比较,请查看这篇文章:Memory Ordering in Modern Microprocessors。您可以看到 Itanium (IA-64) 比 IA-32 架构进行更多的重新排序:

【讨论】:

  • 这是可能的。所以第一个操作是用常量改变变量。其次是获取变量的值并设置为 r[x]。所以它可能会在执行第一次操作的同时尝试获取变量值。
  • 编译器也可能是罪魁祸首,c.f. '如何使用“良性”数据竞争错误编译程序' - usenix.org/events/hotpar11/tech/final_files/Boehm.pdf.
  • @Brian:虽然很有趣,但由于信号处理语义,该论文中的许多示例对于 C/POSIX 来说都是错误的(编译器可以进行的无效转换),特别是如果省略的代码包含调用到sigprocmask
【解决方案2】:

在内存一致性模型较弱的处理器(例如 SPARC、PowerPC、Itanium、ARM 等)上,由于在没有显式内存屏障指令的情况下写入时缺乏强制缓存一致性,可能会发生上述情况。所以基本上Core1y 之前看到x 上的写入,而Core2x 之前看到y 上的写入。在这种情况下,不需要完整的栅栏指令......基本上,您只需要在这种情况下强制执行写入或释放语义,以便所有写入都已提交并对所有处理器可见,然后再对那些已被读取的变量进行读取。写给。具有强内存一致性模型(如 x86)的处理器架构通常不需要这样做,但正如 Groo 指出的那样,编译器本身可以重新排序操作。您可以在 C 和 C++ 中使用 volatile 关键字来防止编译器在给定线程中对操作进行重新排序。这并不是说volatile 将创建线程安全代码来管理线程之间读写的可见性......这将需要内存屏障。因此,虽然使用volatile 仍然可以创建不安全的线程代码,但在给定的线程中,它将在已编译的机器代码级别强制执行顺序一致性。

【讨论】:

  • CPU 也可以重新排序指令(同时保持与新排序相关的缓存一致性)。
  • Volatile 不会阻止重新排序,除非旧编译器使用“volatile”作为关键字来完全禁用优化器。你需要一个内存栅栏指令。
  • 如果 volatile 没有阻止编译器在给定线程中重新排序,那么在内存映射 I/O 中使用绝对没有意义......编译器仍然会重新排序读取和写入,你会得到各种未定义的硬件行为。 volatile 关键字不会阻止 CPU 重新排序指令,也不会强制线程之间读写的可见性......这需要内存屏障......但它确实阻止编译器本身对给定的操作重新排序源代码中定义的线程。
【解决方案3】:

这就是为什么有人说:Threads Considered Harmful

问题是两个线程都没有强制它的两个语句之间的任何顺序,因为它们不是相互依赖的。

  • 编译器知道 xy 没有别名,因此不需要对操作进行排序。

  • CPU 知道 xy 没有别名,因此它可能会重新排序它们以提高速度。发生这种情况的一个很好的例子是当 CPU 检测到write combining 的机会时。如果它可以在不违反其一致性模型的情况下合并一个写入与另一个写入。

相互依赖看起来很奇怪,但实际上与任何其他竞争条件没有什么不同。直接编写共享内存线程代码非常困难,这就是为什么要开发并行语言和消息传递并行框架的原因,以便将并行危害隔离到小内核并消除应用程序本身的危害。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-23
    • 2018-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多