【Windows】線程漫談——線程同步之原子訪問

來源:互聯網
上載者:User

 本系列意在記錄Windwos線程的相關知識點,包括線程基礎、線程調度、線程同步、TLS、線程池等。

多線程同步的難題

我們知道單核處理器同一時刻只能處理一條指令,作業系統通過時間片調度實現了多任務和多線程。在這個過程中,作業系統隨時會中斷一個線程(這種中斷是以指令為單位的),也就是說完全有可能在一個不確定的時候,線程用完了時間片,控制權交給了另一個線程,另一個線程用完時間片,控制權轉回,但是這一進一出有可能一個被共用的全域變數的值已經變了!這也許會帶來災難性的後果,也許不會。因此,站在系統層面考慮,每當屬於線程的時間片用完之後,系統要把當前CPU寄存器的值(比如,指令寄存器,棧指標寄存器)寫入線程核心對象以“儲存現場”,當線程再次獲得時間片後,應該從核心對象中把上一次的“現場”恢複到CPU寄存器中。

需要強調的是,線程被中斷的時間完全不確定。對於CPU來說,真正的“原子操作”應該是一條指令,而不是進階語言的語句。假設 g_x++ 這樣的C語句操作需要如下的彙編指令:

MOV EAX, [g_x]INC EAXMOV [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的電腦,情況卻又不一樣了。例如:

  1. CPU1 讀取了一個位元組,以及它和它相鄰的位元組被讀入 CPU1 的快取。
  2. CPU2 做了上面同樣的工作。這樣 CPU1 , CPU2 的快取擁有同樣的資料。
  3. CPU1 修改了那個位元組,被修改後,那個位元組被放回 CPU1 的快取行。但是該資訊並沒有被寫入RAM 。
  4. 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

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.