并行编程——内存模型之顺序一致性

1  定义程序员

Sequential consistency , 简称 SC,定义以下编程

… the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program [lamport]多线程

下面用一个小例子说明这个定义的意思:架构

假设咱们有两个线程(线程1和线程2)分别运行在两个CPU上,有两个初始值为0的全局共享变量x和y,两个线程分别执行下面两条指令:app

初始条件: x = y = 0;函数

表 1.1  CC 示意图性能

线程 1优化

线程 2this

x = 1;spa

y=1;

r1 = y;

r2 = x;

 由于多线程程序是交错执行的,因此程序可能有以下几种执行顺序:

 表 1.2  CC示意图2

Execution 1

Execution 2

Execution 3

x = 1;

r1 = y;

y = 1;

r2 = x;

结果:r1==0 and r2 == 1

y = 1;

r2 = x;

x = 1;

r1 = y;

结果: r1 == 1 and r2 == 0

x = 1;

y = 1;

r1 = y;

r2 = x;

结果: r1 == 1 and r2 == 1

Execution 1

Execution 2

Execution 3

固然上面三种状况并没包括全部可能的执行顺序,可是它们已经包括全部可能出现的结果了,因此咱们只举上面三个例子。咱们注意到这个程序只可能出现上面三种结果,可是不可能出现r1==0 and r2==0的状况。

SC其实就是规定了两件事情:
1)每一个线程内部的指令都是按照程序规定的顺序(program order)执行的(单个线程的视角)
2)线程执行的交错顺序能够是任意的,可是全部线程所看见的整个程序的整体执行顺序都是同样的(整个程序的视角)

第一点很容易理解,就是说线程1里面的两条语句必定在该线程中必定是x=1先执行,r1=y后执行。第二点就是说线程1和线程2所看见的整个程序的执行顺序都是同样的,举例子就是假设线程1看见整个程序的执行顺序是咱们上面例子中的Execution 1,那么线程2看见的整个程序的执行顺序也是Execution 1,不能是Execution 2或者Execution 3。

有一个更形象点的例子。伸出你的双手,掌心面向你,两个手分别表明两个线程,从食指到小拇指的四根手指头分别表明每一个线程要依次执行的四条指令。SC的意思就是说:
(1)对每一个手来讲,它的四条指令的执行顺序必须是从食指执行到小拇指
(2)你两个手的八条指令(八根手指头)能够在知足(1)的条件下任意交错执行(例如能够是左1,左2,右1,右2,右3,左3,左4,右4,也能够是左1,左2,左3,左4,右1,右2,右3,右4,也能够是右1,右2,右3,左1,左2,右4,左3,左4)

其实说简单点,SC就是咱们最容易理解的那个多线程程序执行顺序的模型。CC 保证的是对一个地址访问的一致性,SC保证的是对一系列地址访问的一致性。

2  几种顺序约束

顺序的内存一致性模型为咱们提供了一种简单的而且直观的程序模型。可是,这种模型实际上阻止了硬件或者编译器对程序代码进行的大部分优化操做。为此,人们提出了不少松弛的(relaxed)内存顺序模型,给予处理器权利对内存的操做进行适当的调整。例如Alpha处理器,PowerPC处理器以及咱们如今使用的x86, x64系列的处理器等等。下面是一些内存顺序模型

2.1  TSO (总体存储定序)

  • 数据载入间的执行顺序不可改变。
  • 数据存储间的顺序不可改变。
  • 数据存储同相关的它以前的数据载入间的顺序不可改变。
  • 数据载入同其相关的它以前的数据存储的顺序能够改变。
  • 向同一个地址存储数据具备全局性的执行顺序。
  • 原子操做按顺序执行。
  • 这方面的例子包括x86 TSO26和SPARC TSO.

2.2  PSO (部分存储定序)

  • 数据载入间的执行顺序不可改变。
  • 数据存储间的执行顺序能够改变。
  • 数据载入同数据存储间相对顺序能够改变。
  • 向同一个地址存储数据具备全局性的执行顺序。
  • 原子操做同数据存储间的顺序能够改变。
  • 这方面的例子包括SPARC PSO.

2.3  RMO (宽松内存定序)

  • 数据载入间的顺序能够改变。
  • 数据载入同数据存储间的顺序能够改变。
  • 数据存储间的顺序能够改变。
  • 向同一个地址存储数据具备全局性的执行顺序。
  • 原子操做同数据存储和数据载入间的顺序能够改变。
  • 这方面的例子包括Power27和ARM.7

图 1  一些体系架构的内存顺序标准

 

图 2  强内存顺序模型和弱内存顺序模型一些例子,

最左边的内存顺序一致性约束越弱,右边的约束是在左边的基础上加上更多的约束,X86/64 算是比较强的约束。

 

3  乱序执行和内存屏障

任何非严格知足SC规定的内存顺序模型都产生所谓乱序执行问题,从编程人员的代码,到编译器,到CPU运行,中间可能至少须要对代码次序作三次调整,每一次调整都是为了最终执行的性能更高。以下图

图 3  编译乱序和运行乱序

串行时代,编译器和CPU对代码所进行的乱序执行的优化对程序员都是封装好了的,无痛的,因此程序员不须要关心这些代码在执行时被乱序成什么样子。在单核多线程时代,mutex , semaphore 等机制在实现的时候考虑了编译和执行的乱序问题,能够保证关键代码区不会被乱序执行。在多核多线程时代,大部分状况下跟单核多线程是相似的,经过锁调用能够保证共享区执行的顺序性。但某种状况下,好比本身编写无锁程序,则会被暴露到这个问题面前。

 下面经过一个例子解释乱序执行和内存屏障这两个概念。

[来源:http://preshing.com/20120625/memory-ordering-at-compile-time]

示例代码:

int A, B;

void foo()

{

    A = B + 1;

    B = 0;

}

普通编译选项:

$ gcc -S -masm=intel foo.c

$ cat foo.s

        ...

        mov     eax, DWORD PTR _B  (redo this at home...)

        add     eax, 1

        mov     DWORD PTR _A, eax

        mov     DWORD PTR _B, 0

加上 -o2 优化编译选项,能够看到,B的赋值操做顺序变了

$ gcc -O2 -S -masm=intel foo.c

$ cat foo.s

        ...

        mov     eax, DWORD PTR B

        mov     DWORD PTR B, 0

        add     eax, 1

        mov     DWORD PTR A, eax

        ...

上述状况在某些场景下致使的后果是不可接受的,好比下面这段伪代码中,生产者线程执行于一个专门的处理器之上,它先生成一条消息,而后经过更新ready的值,向执行在另一个处理器之上的消费者线程发送信号,因为乱序执行,这段代码在目前大部分平台上执行是有问题的:

处理器有可能会在将数据存储到message->value的动做执行完成以前和/或其它处理器可以看到message->value的值以前,执行consume函数对消息进行接收或者执行将数据保存到ready的动做。

图 4  乱序执行

回到以前的例子,加上一句内存屏障命令

int A, B;

 

void foo()

{

    A = B + 1;

    asm volatile("" ::: "memory");

    B = 0;

}

依然采用 o2 优化编译选项,发现此次B的赋值操做顺序没有变化

$ gcc -O2 -S -masm=intel foo.c

$ cat foo.s

        ...

        mov     eax, DWORD PTR _B

        add     eax, 1

        mov     DWORD PTR _A, eax

        mov     DWORD PTR _B, 0

        ...

在内存顺序一致性模型不够强的多核平台上,例子2的正确实现应该是下面这种,须要加上两个内存屏障语句。

图5  内存屏障

X86 的内存屏障 #define barrier() __asm__ __volatile__("": : :"memory")

更多X86内存屏障请参考 : http://blog.csdn.net/cnctloveyu/article/details/5486339

相关文章
相关标签/搜索