本系列意在記錄Windwos線程的相關知識點,包括線程基礎、線程調度、線程同步、TLS、線程池等。
多線程同步的難題
我們知道單核處理器同一時刻只能處理一條指令,操作系統通過時間片調度實現了多任務和多線程。在這個過程中,操作系統隨時會中斷一個線程(這種中斷是以指令為單位的),也就是說完全有可能在一個不確定的時候,線程用完了時間片,控制權交給了另一個線程,另一個線程用完時間片,控制權轉回,但是這一進一出有可能一個被共享的全局變量的值已經變了!這也許會帶來災難性的后果,也許不會。因此,站在系統層面考慮,每當屬於線程的時間片用完之后,系統要把當前CPU寄存器的值(比如,指令寄存器,棧指針寄存器)寫入線程內核對象以“保存現場”,當線程再次獲得時間片后,應該從內核對象中把上一次的“現場”恢復到CPU寄存器中。
需要強調的是,線程被中斷的時間完全不確定。對於CPU來說,真正的“原子操作”應該是一條指令,而不是高級語言的語句。假設 g_x++ 這樣的C語句操作需要如下的匯編指令:
MOV EAX, [g_x] INC EAX MOV [g_x], EAX
可能執行完第二句指令,新的g_x值還沒有回寫內存,線程的時間片到了,控制權交給了另外一個線程的,另一個線程也要操作g_x,那么結果將是不可預知的。
可見線程同步的難度似乎比我們想象的要大一些。幸好,Windows或各種語言或者各種類庫為我們提供了很多線程同步的方法。這篇開始討論Win32下的線程同步的話題。
原子訪問:Interlocked系列函數
為了解決上面對g_x++這樣的操作的原子訪問(即保證g_x++不會被打斷),可以用如下方法:
long g_x = 0; DWORD WINAPI ThreadFunc1(PVOID pvParam){ InterlockedExchangeAdd(&g_x,1); return(0); } DWORD WINAPI ThreadFunc2(PVOID pvParam){ InterlockedExchangeAdd(&g_x,1); return(0); }
上面代碼的InterlockedExchangeAdd保證加法運算以“原子訪問”的方式進行。InterlockedExchangeAdd的工作原理根據不同的CPU會有所不同。但是,我們必須保證傳給這些Interlocked函數的變量地址是經過對齊的。
所謂對齊,是指數據的地址模除數據的大小應該為0,比如WORD的起始地址應該能被2整除,DWORD的地址能被4整除。x86架構的CPU能夠自動處理數據錯位,而IA-64的處理器不能處理,而會將錯誤拋給Windows,Windows能決定是拋出異常還是幫助CPU處理錯位。總之,數據錯位不會導致錯誤,但由於CPU將至少多耗費一個讀內存操作,因此將影響程序的性能。
InterlockedExchange用於以原子的方式設置一個32位的值,並返回它之前的值,可以用來實現旋轉鎖(spinlock):
//全局變量指示共享資源是否被占用 BOOL g_fResourceInUse = FALSE; ... void Func1(){ //等待共享資源釋放 while ( InterlockedExchange ( &g_fResourceInUse, TRUE ) == TRUE ) Sleep(0); //訪問共享資源 ... //不再需要共享資源時釋放 InterlockedExchange ( &g_fResourceInUse, FALSE ); }
while循環不停的進行,並且設置g_fResourceInUse為TRUE,如果返回值為TRUE表示資源已經被占用,於是線程Sleep(0)意味着線程立即放棄屬於自己的時間片,這樣將導致CPU調度其他線程。如果返回值為FLASE,表示資源當前沒有被占用,可以訪問共享資源。不過在使用這項技術的時候要很小心,因為旋轉鎖將浪費CPU時間。
高速緩存行與volatile
眾所周知,CPU擁有高速緩存,CPU高速緩存的大小是評判CPU性能的一個指標。現如今的CPU一般擁有3級的緩存,CPU總是優先從一級緩存中中讀取數據,如果讀取失敗則會從二級緩存讀取數據,最后從內存中讀取數據。CPU的緩存由許多緩存行組成,對於X86架構的CPU來說,高速緩存行一般是32個字節。當CPU需要讀取一個變量時,該變量所在的以32字節分組的內存數據將被一同讀入高速緩存行,所以,對於性能要求嚴格的程序來說,充分利用高速緩存行的優勢非常重要。一次性將訪問頻繁的32字節數據對齊后讀入高速緩存中,減少CPU高級緩存與低級緩存、內存的數據交換。
但是對於多CPU的計算機,情況卻又不一樣了。例如:
- CPU1 讀取了一個字節,以及它和它相鄰的字節被讀入 CPU1 的高速緩存。
- CPU2 做了上面同樣的工作。這樣 CPU1 , CPU2 的高速緩存擁有同樣的數據。
- CPU1 修改了那個字節,被修改后,那個字節被放回 CPU1 的高速緩存行。但是該信息並沒有被寫入RAM 。
- CPU2 訪問該字節,但由於 CPU1 並未將數據寫入 RAM ,導致了數據不同步。
當然CPU設計者充分考慮了這點,當一個 CPU 修改高速緩存行中的字節時,計算機中的其它 CPU會被通知,它們的高速緩存將視為無效。於是,在上面的情況下, CPU2 發現自己的高速緩存中數據已無效, CPU1 將立即把自己的數據寫回 RAM ,然后 CPU2 重新讀取該數據。 可以看出,高速緩存行在多處理器上會導致一些不利。
以上背景知識對於我們編程至少有如下兩個意義:
1、有些編譯器會對變量進行優化,這種優化可能導致CPU對變量的讀取指令始終指向高速緩存,而不是內存。這樣的話,當一個變量被多個線程共享的時候,可能會導致一個線程對變量的設置始終無法在另一個線程中體現,因為另一個線程在另一個CPU上運行,並且變量的值在該CPU的高速緩存中!volatile關鍵字告訴編譯器生成的代碼始終從內存中讀取變量,而不要做類似優化。
2、在多CPU環境下,合理的設置高速緩存對齊,以使得CPU之間的高速緩存同步動作盡量的少發生,以提升性能。要對齊高速緩存,首先要知道目標CPU的高速緩存行的大小,然后用__declspec(align(#))來告訴編譯器為變量或結構設置指定符合高速緩存行大小的數據大小,例如:
struct CACHE_ALIGN S1 { // cache align all instances of S1 int a, b, c, d; }; struct S1 s1; // s1 is 32-byte cache aligned
更多內容可參見:http://msdn.microsoft.com/en-us/library/83ythb65.aspx
具體的,高速緩存行對齊的目標可以是:在結構中,把經常讀操作的字段和經常寫操作的字段分開,使得讀操作的字段與寫操作的字段出現在不同的高速緩存行中。這樣就減少了CPU高速緩存行同步的次數,一定程度上提升了性能。
勞動果實,轉載請注明出處: http://www.cnblogs.com/P_Chou/archive/2012/06/17/interlocked-in-thread-sync.html