最近一個項目中用到了peterson算法來作臨界區的保護,簡簡單單的十幾行代碼,就能實現兩個線程對臨界區的無鎖訪問,確實很精煉。可是在這不是來分析peterson算法的,在實際應用中發現peterson算法並不能對臨界區進行互斥訪問,也就是說兩個線程仍是有可能同時進入臨界區。可是按照代碼的分析,明明能夠實現互斥訪問的呀,這是怎麼回事呢?ios
首先用一個測試程序來檢驗一下。臨界區是對一個全局變量的自加一運算,兩個線程各加一百萬次,最後結果應該是兩百萬。因爲自加一運算不是原子的,若是兩個線程同時進入臨界區,最後的結果就會少於兩百萬。算法
#include <iostream> #include <pthread.h>
using namespace std; static volatile bool flag[2] = {false, false}; static volatile int turn = 0; static volatile int gCount = 0; void procedure0() { flag[0] = true; turn = 1; while (flag[1] && (turn == 1)); gCount++; flag[0] = false; } void procedure1() { flag[1] = true; turn = 0; while (flag[0] && (turn == 0)); gCount++; flag[1] = false; } void* ThreadFunc0(void* args) { int i; for (i = 0; i<1000000; i++) procedure0(); return NULL; } void* ThreadFunc1(void* args) { int i; for (i = 0; i<1000000; i++) procedure1(); return NULL; } int main() { pthread_t pid0, pid1; if (pthread_create(&pid0, 0, &ThreadFunc0, NULL)) { cout << "Create thread0 failed." << endl; return 1; } if (pthread_create(&pid1, 0, &ThreadFunc1, NULL)) { cout << "Create thread1 failed." << endl; return 1; } pthread_join(pid0, NULL); pthread_join(pid1, NULL); cout << gCount << endl; if (gCount == 2000000) cout << "Success" << endl; else cout << "Fail" << endl; return 0; }
x86平臺peterson鎖失效 arm平臺peterson鎖失效ide
這個測試程序在Linux上用gcc編譯,不管用O0,O1,O2編譯選項,我試過x86平臺,Arm平臺,結果都有可能小於兩百萬,也就是這樣實現的peterson鎖不能阻止兩個線程同時進入臨界區。緣由在於現代的編譯器和多核CPU由於優化代碼的緣由,最擅長的事情就是指令亂序執行。編譯器作的是靜態亂序優化,CPU作的是動態亂序優化。簡單來講,就是指令最終在CPU的執行順序和咱們在程序中寫的順序多是截然不同的。固然這種亂序執行是要在保證最終執行結果正確的前提下的,大多數狀況下都不會引發問題,咱們對指令的亂序執行也毫無感知。可是在一些特殊的狀況下,好比peterson算法裏,亂序優化可能會引發問題。函數
一般狀況下,亂序優化均可以把對不一樣地址的load操做提到store以前去,我想這是由於load操做若是cache命中的話,要比store快不少。以線程0爲例,看這3行。測試
flag[0] = true; turn = 1; while (flag[1] && (turn == 1));
前兩行是store,第三行是load。可是對同一變量turn的store再load,亂序優化是不可能對他們交換順序的。可是flag[0]和flag[1]是不一樣的變量,先store後load就可能被亂序優化成先load flag[1],再store flag[0]。假設兩個線程都已退出臨界區,準備再次進入,此時flag[0]和flag[1]都是false。按亂序執行先load,兩個線程都會有while條件爲假,則同時均可以進入了臨界區,互斥失效!這就是在有些狀況下要保持代碼的順序一致性的重要。優化
這個問題怎麼解決呢?也很簡單,就是使用內存柵欄(memory barrier)。顧名思義,他就像個柵欄同樣擺在兩段代碼之間,阻止編譯器或者CPU在這兩段代碼之間進行亂序優化。在x86平臺上,阻止編譯器的靜態亂序優化的彙編代碼是spa
asm volatile("" ::: "memory");
可是它不能阻止CPU運行時的亂序優化。在這裏咱們須要的不單單是阻止靜態亂序,還要阻止動態亂序。x86的動態內存柵欄彙編命令有三條,分別是lfence,sfence和mfence,分別表示load柵欄,store柵欄和讀寫柵欄。也就是lfence只能保證lfence以前的讀命令不和它以後的讀命令發生亂序。sfence保證sfence以前的寫命令不和它以後的寫命令發生亂序。mfence保證了它先後的讀寫命令不發生亂序。這裏咱們須要用mfence,不過實際上我是了sfence也是能夠的,可是lfence不行。線程
flag[0] = true;
turn = 1; asm("mfence"); while (flag[1] && (turn == 1));
在中間插入一行內存柵欄指令,這樣peterson測試程序執行纔是徹底正確的。code
在arm平臺,相應的內存柵欄指令有三條,dmb(data memroy barrier),dsb(date synchronization barrier)和isb(instruction synchronization barrier)。dmb保證在dmb以前的內存訪問指令在它以後的內存訪問指令以前完成,也就是阻止了亂序。dsb更嚴格一些,保證在dsb完成以前,全部它以前的指令都執行完成。isb最嚴格,它會清空處理器的流水線,固然就能保證以前的全部指令執行完,它以後的指令必須從cache或內存獲取。在這裏咱們用dmb就足夠了,dmb指令帶有參數。用來表達該barrier生效的Shareability Domain(NSH表示Non-shareable、ISH表示Inner Shareable、OSH表示Outer Shareable、SY表示Full system,缺省是SY)和內存操做類型(LD表示讀操做,ST表示寫操做,缺省表示讀寫操做),好比DMB ISHST 表示對Inner Shareability Domain的讀寫操做生效。在peterson算法這裏是能保證正常工做的,或者直接用dmb sy。blog
flag[0] = true; turn = 1; asm("dmb ishst");
//asm("dmb sy");
while (flag[1] && (turn == 1));
因爲彙編指令和平臺相關,移植不便。4.4.0和以後版本的gcc方便地提供了__sync_synchronize
函數完成內存柵欄指令。
flag[0] = true; turn = 1; __sync_synchronize(); while (flag[1] && (turn == 1));