[译]如何设计多核计算机以正确执行多进程程序

论文原题:How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programsbash

摘要 - 不少大型的有序计算机不必定是按照程序指定的顺序来执行的。一次执行,若是其结果和按顺序执行的结果一致,则是一次正确的执行。不过,对于一个多核处理器,每一个核都正确执行,却不必定能保证整个程序是正确执行的。要保证计算机正确的处理多进程程序,还须要知足一些额外的条件。并发

一个高速运行的处理器不必定按照程序指定的顺序执行。一个处理器必须知足下面的条件,才能保证正确的执行:处理器的执行结果,必须和按照程序指定顺序执行的结果一致。性能

倒不如直接说,处理器必须按照程序指定的顺序执行。或许理论上还有不按顺序执行,也能保证正确结果的方法,不过这种方法岂不难以解释fetch

一个知足该条件的处理器就能够被称做是有序的(sequential)。而对于多个处理器访问同一块内存的场景,则必须知足下面的条件才能保证多进程并发的正确性:spa

  • 任何一次执行的结果,和全部处理器按照某个顺序执行的结果一致;
  • 在这”某个顺序执行“中单独看每一个处理器,则每一个处理器也都是按照程序指定的顺序执行的。

一个多核处理器若是知足了这样的条件,就被称做是顺序一致的(sequentially consistent)设计

仅仅是每一个处理器保证本身的有序性,是没法保证最终多核计算机的顺序一致的。咱们在这里介绍一种有序处理器和内存模块协做的方法,以确保最终的多核处理器是顺序一致的。code

咱们假设计算机由一组处理器和一组内存模块构成,而处理器之间只经过内存模块进行交互(任何特殊的寄存器也当作是内存模块)。那么咱们惟一须要关心的处理器操做就是往内存发起“读”或“写”的请求。咱们假设每一个处理器都会发起一系列的读写请求(有时候,处理器得等当前请求执行完后,才能继续下一步工做,但咱们不用关心这个)。队列

为了阐述这个问题,咱们来考虑一个简单的两进程互斥协议。每一个进程都有一个临界区,协议的目的是保证任什么时候候只有一个进程可以在临界区内执行。协议设计以下:进程

process1
  a := 1;
  if b = 0 then critical section;
    a := 0
  else ··· 
  fi
复制代码
process2
  b := 1;
  if a = 0 then critical section;
    b := 0
  else ··· 
  fi
复制代码

else里的内容保证进程最终可以进入临界区(译者注:也就是不会死锁吧),可是这和咱们须要讨论的问题无关。当这个程序在顺序一致的多核计算机上执行时,两个进程是不能同时在临界区内执行的。事件

咱们首先注意到,一个有序处理器可能以任意的顺序执行“b:=1"和process1中的"fetch b"(当只考虑process1时,这两个操做的顺序如何却是没什么关系)。不过,显而易见,若是先执行"fetch b"会致使两个进程能同时进入临界区执行。这引出了咱们对于多核计算机的第一个必要条件:

必要条件R1:每一个处理器必须按照程序指定的顺序处理内存请求。

要知足必要条件R1其实是个复杂的问题。好比有时一个值要计算好了,才能写到内存,这样写内存的耗时会比较长。而处理器每每能够很快发起读内存的请求,此时,上一次写内存的请求却不必定完成了。为了最小化等待时间,处理器能够先向内存发起写请求,而不携带具体要写的值。固然,内存要等接受到这个具体的值,才能完成最终的写操做。

举个例子

a = a + 1
get b
复制代码

对于上面的程序,处理器能够采用两种方式:

  1. 处理器先计算出 a + 1,再将写a的请求,以及计算出的a的新值一块儿发给内存。以后处理器再向内存发起读b的请求;
  2. 先对内存发起写a的请求,可是不携带要写的值,以后,便可发起读b的请求。处理器可在以后的某个时间,计算出a + 1来,发给内存,以完成a = a + 1。

感受这种讨论蛮奇怪的。

必要条件R1并不足以保证正确的执行。咱们假设每一个内存模块都有好几个端口,每一个端口服务于一个处理器(或是I/O通道)。咱们让 a 和 b 的值存储在不一样的内存模块中,而且考虑按以下顺序发生的事件:

  1. 处理器1发送 a := 1 的请求给内存模块1,内存模块1正忙着其余事;
  2. 处理器1发送 fetch b 的请求给内存模块2,内存模块2闲着,当即开始处理此次请求;
  3. 处理器2发送 b := 1 的请求给内存模块2,这个请求得等处理器1的 fetch b 完成了才能执行;
  4. 处理器2发送 fetch a 的请求给内存模块1,内存模块1还没忙完。

这个时候,有两个操做挂在内存模块2上等待执行。若是处理器2的 fetch a 先执行了,那么上文的那个互斥协议就出问题了,由于两个process就同时进入临界区了。这在内存模块采起轮询的调度机制来处理请求时,是有可能发生的。

在这种状况下,由于到达内存模块1的两次请求,并未按照他们被接收到的顺序执行,而致使了错误。这引出了下面的必要条件。

必要条件R2: 一个独立的内存模块在服务全部处理器的请求时,必须基于FIFO队列。发起内存请求,便是将请求加入到FIFO队列。

而必要条件R1则意味着处理器在发起更多的内存请求以前,都必须等待当前的请求入队。所以,若是队列满了,处理器就得等着。若是多个处理器同时尝试将请求入队,那么谁先入队就不要紧了。

注意,若是一次读请求A,读的那块内存,正好写请求B想要改,而且写请求B已经在队列里了,那么读请求A就不须要入队了,直接返回队列中写请求B所要写的值就好了。若是队列中有多个这样的写请求,返回最新入队的那个写请求所要写的值就好了。

必要条件R1和R2保证了若是一个核是有序的,那么多核处理器就是顺序一致性的。为了证实这点,咱们引入了关系符号 "->" 来描述内存请求。 咱们定义 "A -> B" 当且仅当:

  1. A和B是被同一个处理器发起的请求,且A发起在B以前;
  2. A和B请求的是同一个内存模块,而且A在B以前入队。

能够很容易看出来,必要条件R1和R2意味着在内存请求上存在着 "->" 这样一个偏序关系。利用处理器的有序性,咱们能够证实下面这个结论:对同一个值进行的内存读写操做就好像这些操做都是按照某种顺序执行的,所以 A->B 也就意味着A是在B以前执行的。这反过来就证实了多核计算机的顺序一致性。

感受这段证实的意思就是:R1和R2保证了内存请求上的偏序关系"->",而该偏序关系保证了多核计算机的顺序一致性。(emmmmm,有点谜...)

必要条件R2要求内存模块必须用FIFO的策略来响应请求,这就意味着若是队列头是一个写请求,而且须要写的值还没收到,那么内存模块就须要等待,就会处于空闲状态。咱们能够弱化R2,来容许内存在这种状况下能够响应其余请求。咱们只须要要求针对同一块内存单元的全部请求必须按照FIFO的顺序响应既能够了。对不一样内存单元的请求能够是乱序响应的。顺序一致性依然是可以获得保证的,由于这样其实和将每个内存单元都当作一个拥有本身队列的独立内存模块没啥区别。(现实中,这些模块可能会因硬件特质不一样,而有不一样的响应速度和队列容量,但这并不影响顺序一致性)。

为了保证顺序一致性,须要舍弃一些可以提高有序处理器性能的技术。对于一些应用来讲,牺牲性能而追求顺序一致性多是不值得的。此时,咱们必须意识到按照常规的多核程序设计方法设计出来的程序就不必定能正确执行了。此时,咱们必须在机器指令层面设计其余的协议来保证多核之间的同步,可这样,正确性的验证也会变得很是繁杂。

对于顺序一致性,个人理解就是,多核计算机必须按照程序指定的顺序执行操做,而内存必须按照处理器发起请求的顺序处理读写(内存也等于间接的按照程序指定的顺序执行操做了)。 那么顺序一致性的主要用途就是,它保证多核系统的运做是按照程序指定的顺序来的,这样咱们才能无后顾之忧的设计咱们的程序。

相关文章
相关标签/搜索