22

现代 CPU 架构通常会采用性能优化,这可能会导致无序执行。在单线程应用程序中,内存重新排序也可能发生,但它对程序员来说是不可见的,就好像内存是按程序顺序访问的一样。对于 SMP,内存屏障来拯救它,用于强制执行某种内存排序。

我不确定的是单处理器中的多线程。考虑以下示例:当线程 1 运行时,存储 tof可能发生在存储 to 之前x。假设上下文切换发生在f写入之后,就在x写入之前。现在线程 2 开始运行,它结束循环并打印 0,这当然是不可取的。

// Both x, f are initialized w/ 0.
// Thread 1
x = 42;
f = 1;

// Thread 2
while (f == 0)
  ;
print x;

上面描述的场景可能吗?或者是否可以保证在线程上下文切换期间提交物理内存?

根据这个维基

当程序在单 CPU机器上运行时,硬件会执行必要的簿记,以确保程序执行就像所有内存操作都按照程序员指定的顺序(程序顺序)执行一样,因此不需要内存屏障。

虽然它没有明确提到单处理器多线程应用程序,但它包括了这种情况。

我不确定它是否正确/完整。请注意,这可能高度依赖于硬件(弱/强内存模型)。因此,您可能希望在答案中包含您知道的硬件。谢谢。

PS。设备 I/O 等不是我关心的问题。它是一个单核单处理器。

编辑:感谢 Nitsan 的提醒,我们假设这里没有编译器重新排序(只是硬件重新排序),并且线程 2 中的循环没有被优化掉。再次,魔鬼在细节中。

4

8 回答 8

20

作为一个 C++ 问题,答案必须是程序包含数据竞争,因此行为未定义。实际上,这意味着它可以打印 42 以外的内容。

这与底层硬件无关。正如已经指出的那样,循环可以被优化掉,编译器可以重新排序线程 1 中的分配,因此即使在单处理器机器上也可以出现结果。

[我假设对于“单处理器”机器,您的意思是具有单核和硬件线程的处理器。]

您现在说,您想假设编译器重新排序或循环消除不会发生。至此,我们离开了 C++ 领域,真正开始询问相应的机器指令。如果您想消除编译器重新排序,我们可能还可以排除任何形式的 SIMD 指令,并考虑一次只在单个内存位置上运行的指令。

所以本质上thread1有两个存储指令,顺序是store-to-x store-to-f,而thread2有test-f-and-loop-if-not-zero(这可能是多条指令,但涉及一个load-from -f) 然后从-x 加载。

在我知道或可以合理想象的任何硬件架构上,线程 2 将打印 42。

一个原因是,如果单个处理器处理的指令在它们之间不是顺序一致的,那么您几乎无法断言有关程序效果的任何内容。

唯一可能在这里干扰的事件是中断(用于触发抢占式上下文切换)。一个假设的机器在中断时存储其当前执行流水线状态的整个状态并在中断返回时恢复它,可能会产生不同的结果,但这样的机器是不切实际的,afaik 不存在。这些操作会产生相当多的额外复杂性和/或需要额外的冗余缓冲区或寄存器,所有这些都没有充分的理由——除了破坏你的程序。真正的处理器在中断时刷新或回滚当前流水线,这足以保证单个硬件线程上所有指令的顺序一致性。

并且无需担心内存模型问题。较弱的内存模型源自将单独的硬件处理器与它们实际共享的主内存或第 n 级高速缓存分开的单独缓冲区和高速缓存。单个处理器没有类似的分区资源,也没有充分的理由将它们用于多个(纯软件)线程。同样,如果没有单独的处理资源(处理器/硬件线程)来保持这些资源忙碌,那么没有理由使架构复杂化并浪费资源来使处理器和/或内存子系统知道诸如单独的线程上下文之类的东西。

于 2013-01-10T01:38:47.617 回答
5

强内存排序以与程序中定义的完全相同的顺序执行内存访问指令,通常称为“程序排序”。

可以使用较弱的内存排序来允许处理器重新排序内存访问以获得更好的性能,这通常被称为“处理器排序”。

AFAIK,上述情况在英特尔 ia32 架构中是不可能的,其处理器订购禁止此类情况相关规则为(intel ia-32软件开发手册Vol3A 8.2 Memory Ordering):

除了流存储、CLFLUSH 和字符串操作外,写入不会与其他写入重新排序。

为了说明这个规则,它给出了一个类似的例子:

内存位置x,y,初始化为0;

线程1:

mov [x] 1
mov [y] 1

线程2:

mov r1 [y]
mov r2 [x]

r1 == 1 和 r2 == 0 是不允许的

在您的示例中,线程 1在存储 x 之前不能存储 f。

@Eric 回应您的评论。

快速字符串存储指令“stosd”,可能会其操作中乱序存储字符串。在多处理器环境中,当一个处理器存储字符串“str”时,另一个处理器可能会观察到 str[1] 在 str[0] 之前被写入,而逻辑顺序假定是在 str[1] 之前写入 str[0];

但这些说明不会与任何其他商店重新订购。并且必须有精确的异常处理。当 stosd 中间发生异常时,实现可以选择延迟它,以便所有乱序子存储(不一定意味着整个 stosd 指令)必须在上下文切换之前提交。

编辑以解决就好像这是一个 C++ 问题所做的声明:

即使这是在 C++ 的上下文中考虑的,据我了解,标准确认编译器不应重新排序线程 1 中 x 和 f 的分配。

$1.9.14 与完整表达式关联的每个值计算和副作用在与要评估的下一个完整表达式关联的每个值计算和副作用之前进行排序。

于 2013-01-09T21:30:27.183 回答
2

这不是一个真正的 C 或 C++ 问题,因为您明确假设没有加载/存储重新排序,这两种语言的编译器都完全允许这样做。

为了论证而允许该假设,请注意循环可能永远不会退出,除非您:

  • 给编译器一些理由相信可能会改变(例如,通过将其地址传递给一些可以修改它f的非内联函数)
  • 将其标记为 volatile,或
  • 使其成为显式原子类型并请求获取语义

在硬件方面,您不必担心在上下文切换期间“提交”物理内存。两个软件线程共享相同的内存硬件和缓存,因此无论内核之间的一致性/一致性协议是否相关,都不存在不一致的风险。

假设两个商店都已发布,并且内存硬件决定重新订购它们。这到底是什么意思?也许 f 的地址已经在缓存中,因此可以立即写入,但 x 的存储会延迟到获取该缓存行为止。好吧,从 x读取取决于相同的地址,因此:

  • 在提取发生之前加载不会发生,在这种情况下,一个理智的实现必须在排队加载之前发出排队存储
  • 或者负载可以窥视队列并获取 x 的值而无需等待写入

无论如何,考虑到切换线程所需的内核抢占本身将发出任何加载/存储障碍所需的内核调度程序状态的一致性,并且很明显在这种情况下硬件重新排序不会成为问题。


真正的问题(您试图避免)是您假设没有编译器重新排序:这是完全错误的。

于 2013-01-12T22:37:56.997 回答
2

您只需要一个编译器围栏。来自内存屏障的 Linux 内核文档(链接):

在单处理器编译系统上,SMP 内存屏障被简化为编译器屏障,因为它假定 CPU 看起来是自洽的,并且会正确地相对于自身对重叠访问进行排序。

为了扩展这一点,在硬件级别不需要同步的原因是:

  1. 单处理器系统上的所有线程共享相同的内存,因此不会在 SMP 系统上发生缓存一致性问题(例如传播延迟),并且

  2. 如果由于抢占式上下文切换而刷新了流水线,则 CPU 执行流水线中的任何乱序加载/存储指令都将被提交或完全回滚。

于 2013-05-29T10:20:07.117 回答
1

此代码很可能永远不会完成(在线程 2 中),因为编译器可以决定将整个表达式提升出循环(这类似于使用非易失性的 isRunning 标志)。也就是说,您需要担心两种类型的重新排序:编译器和 CPU,两者都可以自由移动存储。请参阅此处:http ://preshing.com/20120515/memory-reordering-caught-in-the-act示例。此时,您上面描述的代码受编译器、编译器标志和特定架构的支配。引用的 wiki 具有误导性,因为它可能表明内部重新排序不受 cpu/编译器的支配,但事实并非如此。

于 2013-01-06T13:20:17.503 回答
1

就 x86 而言,从执行代码的角度来看,乱序存储在程序流程方面是一致的。在这种情况下,“程序流”只是处理器执行的指令流,而不是受限于“在线程中运行的程序”的东西。上下文切换等所需的所有指令都被视为此流程的一部分,因此跨线程保持一致性。

于 2013-01-06T14:46:36.930 回答
0

上下文切换必须存储完整的机器状态,以便在挂起的线程恢复执行之前恢复它。机器状态包括处理器寄存器,但不包括处理器流水线。

如果您假设没有编译器重新排序,这意味着所有“即时”的硬件指令都必须在上下文切换(即中断)发生之前完成,否则它们会丢失并且不会被上下文切换存储机制。这与硬件重新排序无关。

在您的示例中,即使处理器交换了两条硬件指令“x=42”和“f=1”,指令指针已经在第二条之后,因此必须在上下文切换开始之前完成两条指令。如果不是这样,由于管道和缓存的内容不是“上下文”的一部分,它们就会丢失。

换句话说,如果导致 ctx 切换的中断发生在 IP 寄存器指向“f=1”之后的指令时,那么该点之前的所有指令都必须完成所有效果。

于 2013-01-10T12:00:43.143 回答
0

从我的角度来看,处理器一个接一个地获取指令。在您的情况下,如果在“x = 42”之前推测性地执行了“f = 1”,则这意味着这两条指令都已经在处理器的管道中。调度当前线程的唯一可能方法是中断。但是处理器(至少在 X86 上)会在处理中断之前刷新管道的指令。因此无需担心单处理器中的重新排序。

于 2015-07-21T07:20:01.393 回答