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 (總體存儲定序)
2.2 PSO (部分存儲定序)
2.3 RMO (寬鬆內存定序)
圖 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