| 最近看的比較雜,摘了一些人的筆記!隨著多核的日益普及,越來越多的程序將通過多線程并行化的方式來提升性能。然而,編寫正確的多線程程序一直是一件非常困的事情,volatile關鍵字的使用就是其中一個典型的例子。C/C++中的volatile一般不能用于多線程同步在C/C++中,如果想把一個變量聲明為volatile,就相當于告訴編譯器這個變量是“易變的”,他隨時可能在其他地方被修改,所以編譯器不能對其做任何變化:即每次讀寫該變量時都必須對其內存地址直接進行操作,并且所以對該變量的操作都必須嚴格按照程序中規定的順序執行。舉例來說,編譯器的常常做的一種性能優化就是把需頻繁讀取的變量緩存到寄存器中,以提升訪問速度。但如果該變量的值隨時可能在片外被改變的話,那么就有可能出現被緩存的值并不是該變量的最新值情況,從而出現運行錯誤。在這種情況就需要用volatile關鍵字來修飾這個變量,以確保編譯器不會對該變量讀寫操作進行任何緩存優化。另一個例子就是內存映射I/O操作。如下代碼所示:Int *p = get_io_address();Int a, b;A = *p;B = *p;P是一個指向硬件I/O端口的指針,該端口的值在每進行一次讀操作后都會變化。這個程序連續對該端口進行兩次讀取操作已將兩個不同的值分別賦值給a和b。如果不把a和b聲明為volatile的話,編譯器可能會”自作聰明”地認為兩次從p讀取的值都是一樣的,從而把*b=*p優化成b = a,最終導致程序出錯。雖然C/C++中volatile關鍵字對這種“易變“的讀寫操作能起到一定的保護,但他卻并不適用于多線程程序中共享變量的同步操作。究其根源,就在于C/C++標準中并沒有volatile賦予原子性和順序性的語義。原子性下面舉個例子說明原子性。i++這看似原子的語句其實有三個操作組成:將該值從內存地址讀取到寄存器中,對寄存器中的值進行加1操作,最后再將新值寫回內存中,正是因為i++并不是原子的,所以如果兩個線程同時進行i++操作的話仍會產生數據競跑,從而導致i的最終值不等于2.在這種情況下,C/C++中的volatile關鍵字根本無法對該操作的原子性提供任何保障。Volatile int i=0;//線程1I++;//線程2I++;順序性不幸的是,現在C/C++標準中的volatile關鍵字對共享變量操作的順序性也未提供任何保障。以本文中的dekker算法為例:當兩個線程分別執行dekker1和dekker2函數時候,改程序通過對flag1/2和turn的讀寫來實現兩個線程對臨界區中共享變量gCounter的互斥訪問。這個算法的關鍵就在于對flag1/2和turn的讀寫操作是在其寫操作之后進行的,因此它能保證dekker1和dekker2中對gCounterde的操作時互斥的,相當于把gCounter++放到一個臨界區中去了。Dekker算法如下所示:Volatile int flag1 = 0;Volatile int flag2 = 0;Volatile int turn = 1;Volatile int gCounter = 0;Void dekker1(){ Flag1 = 1; Turn = 2;While( (flag2 == 1) && ( turn == 2) ){}//進入臨界區 gCounter++; flag1 = 0; //離開臨界區} Void dekker2(){ Flag2 = 1; Turn = 2;While( (flag1 == 1) && ( turn == 2) ){}//進入臨界區 gCounter++; flag2 = 0; //離開臨界區} 盡管volatile規定編譯器不能對同一變量的所有操作進行亂序優化,但它卻不能阻止編譯器對不同volatile變量間的操作進行亂序優化。例如,編譯器可能把dekker1中的flag2讀操作提到flag1和turn寫操作之前,從而導致對臨界區的互斥訪問失效,最終gCounter++操作就會出現數據競跑現象。事實上,即使編譯器沒有對這個程序做任何優化,volatile 關鍵字也不能阻止多核CPU對該程序的亂序優化。以常見的x86硬件來說,它可以對不同變量x,y的store x --àload y進行亂序優化,把load y操作提到store x操作之前。這樣的話,dekker1中flag2的讀操作還是有可能會被提到flag1和turn的寫操作之前,最終導致錯誤的計算結果。那為什么編譯器和多核CPU會對多線程程序做這樣的亂序優化呢?因為從單核的視角來看,flag1 和 flag2,turn的讀寫操作之間沒有任何依賴關系的,使用編譯器/CPU當然可以對他們進行亂序優化以隱藏一部分的內存訪問延遲,從而更好的利用CPU里的流水線。換句話說,這樣的優化雖從單線程的角度來講沒有錯,但卻違反了設計這個多線程算法時所期望的多線程語義。要是解決這個問題,我們需要解決這個問題,我們需要自己添加內存柵欄以顯式保證順序性,或者干脆去別去實現這樣的算法,轉而使用類似pthread_mutex_lock這樣的加鎖操作來實現互斥訪問。綜合上述,由于現有的C/C++標準中并沒有對volatile添加原子性和順序性的語義,所以絕大部分C/C++程序中使用volatile來進行多線程同步的用法是錯誤的。其實,我們之所以想用volatile變量進行同步,無非是因為鎖,條件變量等方式的開銷太大,所以想有一種輕量級的,高效的同步機制。 |
新聞熱點
疑難解答