处理乱序执行

时间:2010-01-28 23:31:49

标签: multithreading

我最近偶然发现了Wikipedia article。根据我的多线程经验,我知道程序能够随时在线程之间切换线程所引起的众多问题。但是,我从来不知道编译器和硬件优化可以以保证适用于单个线程的方式重新排序操作,但不一定用于多线程。任何人都可以解释如何正确处理多线程环境中重新排序操作的可能性吗?

更新:我最初意外地链接到Out-of-Order Execution文章而不是Memory barrier文章,该文章更好地解释了问题。

12 个答案:

答案 0 :(得分:8)

让我问一个问题:给定一个程序代码(比如它是一个单线程的应用程序),正确的执行是什么?直观地,按照代码指定的CPU按顺序执行将正确。这种顺序执行的错觉就是程序员所拥有的。

然而,现代CPU不遵守这样的限制。除非违反依赖(数据依赖,控制依赖和内存依赖),否则CPU以无序方式执行指令。但是,程序员完全隐藏它。程序员永远无法看到CPU内部发生了什么。

编译器也利用这一事实。如果程序的语义(即代码中固有的依赖性)可以保留,编译器会重新排序任何可能的指令以获得更好的性能。一个值得注意的优化是代码提升:编译器可以提升加载指令以最小化内存延迟。但是,不要担心,编译器保证其正确性;在任何情况下,编译器都不会因为这样的指令重新排序而导致程序崩溃,因为编译器必须至少保留依赖性。 (但是,编译器可能有错误:-)

如果您只考虑单线程应用程序,对于单线程情况,您不需要担心编译器和CPU的这种无序执行。

(为了了解更多,我建议你看一下ILP(instruction-level parallelism)的概念。单线程性能主要取决于你可以从单个线程中提取多少ILP。所以,CPU和编译器都做无论他们为了更好的表现而做些什么。)

但是,当您考虑多线程执行时,它有一个潜在的问题,称为内存一致性问题。直观的程序员有顺序一致性的概念。但是,现代多核架构正在进行肮脏和积极的优化(例如,缓存和缓冲区)。在现代计算机体系结构中很难实现低开销的顺序一致性。因此,由于内存加载和存储的无序执行,可能会出现非常混乱的情况。您可能会观察到一些装载和存储已经无序执行。阅读一些与宽松内存模型相关的文章,例如Intel x86's memory model(阅读第8章,内存订购,英特尔64的第3A卷和IA-32架构软件开发人员手册)。在这种情况下需要内存障碍,您必须强制执行内存指令的顺序才能正确。

对问题的回答:简而言之,回答这个问题并不容易。由于内存一致性模型,没有好的工具可以检测到这种无序和有问题的行为(尽管有研究论文)。因此,简而言之,您甚至很难在代码中找到此类错误。不过,我强烈建议您阅读double-checked locking及其detailed paper上的文章。在双重检查锁定中,由于放松的内存一致性和编译器的重新排序(注意编译器不知道多线程行为,除非您明确指定了内存障碍),否则可能导致错误行为。

总之:

  • 如果您只是在处理单线程程序,那么您无需担心无序行为。
  • 在多核上,您可能需要考虑内存一致性问题。但是,当你真的需要担心内存一致性问题时,实际上很少见。大多数情况下,数据竞争死锁原子性违规会导致多线程程序失效。

答案 1 :(得分:8)

我将以高级语言解决您的问题,而不是讨论CPU流水线优化。

  

任何人都可以解释如何正确处理多线程环境中重新排序操作的可能性吗?

大多数(如果不是全部)现代高级多线程语言提供了用于管理编译器重新排序指令逻辑执行的潜力的构造。在C#中,这些包括字段级构造(volatile修饰符),块级构造(lock关键字)和命令式构造(Thead.MemoryBarrier)。

volatile 应用于字段会导致对CPU /内存中该字段的所有访问以与指令序列中出现的相同的相对顺序执行(源代码)

在代码块周围使用 lock 会导致所附的指令序列以与父代码块相同的相对顺序执行。

Thread.MemoryBarrier 方法向编译器指示CPU不得在指令序列中的此点周围重新排序内存访问。这为专门要求提供了更先进的技术。

以增加复杂性和性能的顺序描述上述技术。与所有并发编程一样,确定何时何地应用这些技术是一项挑战。在同步对单个字段的访问权限时,volatile关键字将起作用,但可能会被证明是过度的。有时您只需要同步写入(在这种情况下,ReaderWriterLockSlim将以更好的性能完成相同的事情)。有时您需要快速连续多次操作该字段,或者您必须检查字段并有条件地操纵它。在这些情况下,lock关键字是更好的主意。有时,您有多个线程在非常松散同步的模型中操作共享状态以提高性能(通常不推荐)。在这种情况下,精心放置的内存屏障可以防止在线程中使用过时和不一致的数据。

答案 2 :(得分:3)

让我们明白 - 乱序执行是指处理器执行管道,而不是编译器本身,正如您的链接清楚地表明的那样。
乱序执行是大多数现代CPU流水线采用的策略,允许它们动态地重新排序指令,通常最小化读/写停顿,这是现代硬件上最常见的瓶颈,因为CPU执行速度和内存之间存在差异延迟(即我的处理器可以获取和处理的速度与我将结果更新回RAM的速度相比) 所以这主要是硬件功能而不是编译器功能 如果您通常使用memory barriers知道自己在做什么,则可以覆盖此功能。 Power PC有一个名为eieio的强有力的指令(强制执行i / o),强制CPU清除所有挂起的读写内存 - 这对于并发编程尤其重要(无论是多线程还是多线程)处理器)因为它确保所有CPU或线程已同步所有内存位置的值 如果你想深入了解这一点,那么this PDF是一个很好的(虽然详细的)介绍 HTH

答案 3 :(得分:3)

这不是编译器,而是CPU。 (实际上两者都是,但CPU更难控制。)无论代码如何编译,CPU都会在指令流中向前看,并且不按顺序执行。通常,例如,尽早启动读取,因为内存比CPU慢。 (即尽早开始,希望在你真正需要之前完成阅读)

CPU和编译器都基于相同的规则进行优化:只要它不影响程序的结果 *,假定单线程单处理器环境* ,就重新排序。

所以存在问题 - 它优化了单线程,而不是。为什么?因为否则一切都会慢100倍。真。大多数代码都是单线程(即单线程交互) - 只有小部分需要以多线程方式进行交互。

控制此问题的最佳/最简单/最安全的方法是使用锁 - 互斥锁,信号量,事件等。

只有当你真的,真的需要优化(基于仔细测量)时,你才能看到内存障碍和原子操作 - 这些是用于构建互斥锁等的基础指令,以及使用时正确限制无序执行。

但在进行这种优化之前,请检查算法和代码流是否正确,以及是否可以进一步减少多线程交互。

答案 4 :(得分:2)

编译器不会产生执行错误,它会对其进行优化和重新排序,只要它生成的内容会产生源代码所说的结果。

但是在处理多线程时,这确实会爆炸,尽管这通常与编译器重新排序代码的方式没什么关系(尽管在其他乐观情况下它会使情况更糟。

处理对相同数据进行操作的线程,您需要非常非常小心,并确保使用适当的防护(信号量/互斥/原子操作等)正确保护您的数据

答案 5 :(得分:1)

问题的关键在于,如果你只是刚刚开始处理多线程代码(以至于你明确地谈论线程调度,好像它有些可怕[不是说它不是,但是出于不同的原因]),这种情况发生的程度远低于您需要担心的程度。正如其他人所说的那样,编译器如果不能保证正确性就不会这样做,虽然知道这样的技术存在是好的,除非你编写自己的编译器或做真正的裸机,否则不应该提出问题

答案 6 :(得分:1)

  

但是,我从来不知道编译器和硬件优化可以以保证适用于单个线程的方式重新排序操作,但不一定是多线程。

由于C和C ++都没有强定义的内存模型,编译器可以重新排序可能导致多线程问题的优化。但对于设计用于多线程环境的编译器,它们

多线程代码写入内存,并使用fence来确保线程之间写入的可见性,或者它使用原子操作。

由于原子操作案例中使用的值在单个线程中是可观察的,因此重新排序不会影响它 - 它们必须在原子操作之前正确计算。

用于多线程应用程序的编译器不会在内存栅栏中重新排序。

因此重新排序不会影响行为,或者作为特殊情况被抑制。

如果您已经编写了正确的多线程代码,则编译器重新排序无关紧要。如果编译器不知道内存防护,这只是一个问题,在这种情况下,您可能不应该首先使用它来编写多线程代码。

答案 7 :(得分:1)

编译器和cpu都实现了算法,这些算法确保为给定的执行流保留顺序语义。对于他们不实现所述算法合格的错误。可以安全地假设指令重新排序不会影响程序语义。

如其他地方所述,内存是唯一可能出现非顺序语义的地方;可以通过各种众所周知的机制在那里获得与顺序主义的同步(在汇编级别,存在原子存储器访问指令;更高级别的功能,例如互斥,屏障,自旋锁等,使用原子汇编指令实现)。

回答您的标题:您不处理OOO执行。

答案 8 :(得分:1)

所以基本上你要问的是内存一致性模型。某些语言/环境(如Java和.NET)定义了内存模型,程序员有责任不执行不允许的操作,或导致未定义的行为。如果您不确定“正常”操作的原子性行为,那么最好是安全而不是遗憾,只需使用互斥原语。

对于C和C ++,情况并不那么好,因为那些语言标准没有定义内存模型。不,与不幸的流行观点相反,不稳定并不保证任何原子性。在这种情况下,您必须依赖平台线程库(其中包括执行所需的内存屏障)或编译器/特定于hw的原子内在函数,并希望编译器不会执行任何破坏程序语义的优化。只要你避免在一个函数(或翻译单位,如果使用IPA)中的条件锁定,你应该是相对安全的。

幸运的是,C ++ 0x和下一个C标准正在通过定义内存模型来纠正这个问题。我问了一个与此相关的问题,结果是条件锁定here;该问题包含一些详细讨论该问题的文档的链接。我建议你阅读这些文件。

答案 9 :(得分:0)

  

如何防止出现执行功能的可能性以及在你面前爆炸?

你没有 - 编译器只能改变执行顺序,这样做不会改变最终结果。

答案 10 :(得分:0)

现在大多数编译器都有明确的排序内在函数。 C ++ 0x也有内存排序内在函数。

答案 11 :(得分:-1)

你不应该在不同的线程中运行需要按顺序运行的东西。线程用于并行处理事物,因此如果顺序很重要,则需要按顺序完成。

相关问题