系統中的所有線程都要訪問系統資源,一個線程霸占某個資源,其他需要該資源的線程就不能完成自己的任務;另外如一個線程在讀取某塊內存中的數據,而另一個線程又正在修改這塊內存的值,這同樣不是我們想要的,所以線程之間必須要有一套自己的規則,不然就凌亂了。線程之間需要通信,如A線程霸占某個B線程需要的資源X,在A占用期間,B線程只能等待,或處於掛起狀態,當A線程用完資源X后,系統會告訴線程B,資源X可以用了,或是將處於掛起狀態的線程B喚醒,然后線程B就獲得對資源X的控制權,其他想用資源X的線程就得經歷B剛才的遭遇。當多個線程同時需要某個資源時必須遵守下面兩個規則:
1:多個線程“同時”訪問資源,不能破壞資源的完整性。
2:一個線程需要通知其他線程某項任務已經完成。
原子訪問:Interlocked系列函數。多線程編程大部分情況與原子訪問有關,即一個線程在訪問某個資源時,確保沒有其他線程能訪問該資源。
增量函數InterlockedExchangeAdd結構如下:
InterlockedExchangeAdd(
unsigned long volatile *Addend,//被增量變量的地址
unsigned long Value//增量值
)
Volatile表示每次都成內存中讀取數據,而不會從高速緩存中讀取數據,如一個全局變量,在一個多線程函數中被修改,在多核CPU中,這個變量可能在多個CPU的高速緩存中都有副本,如果不用volatile修飾,那么可能會因為優化的原因,CPU不會讀內存中的數據,而是直接從高速緩存中讀取數據,在這種情況下,很可能這個值已經被修改了,這樣CPU讀取到的不是最新的數據,程序肯定會出錯,用volatile修飾后,這個變量的所有高速緩存就會失效,就不會出現這種問題。在多線程編程中volatile作用非常大,效率也最高。但他就是只能修飾單個變量,不能修飾代碼段。
InterlockedExchangeAdd執行的速度是非常快的,只需要占用幾個CPU周期。用InterlockedExchangeAdd來修改某個變量的值,好像有點大材小用了,因為用Volatile就足夠了,簡單迅速。但在實現旋轉鎖時InterlockedExchange就非常有用。旋轉鎖的代碼大致如下:
bool sourceIsUse=false; void fun() { //一直等待直到資源可用 while(InterlockedExchange(&sourceIsUse,true)==true) { Sleep(0); } //訪問資源的操作 ...... //資源用好了,打開鎖,讓其他等待的資源訪問 InterlockedExchange(&sourceIsUse,false); }
InterlockedExchange:將第一個參數的值修改成第二個參數的值,返回第一個參數原來的值。在第一個線程就來的時候,它順利的闖過了While循環,並上了鎖,導致while始終為true,后來的線程就一直在while里面打轉,當前面的線程用完之后,他就會把鎖打開,然后新來的線程就可以跳出while循環,並上鎖(在等待時一直在上鎖),開鎖獨占資源了,新來的線程又開始等待。就像大廈前門的旋轉門,一撥人進去之后,后面的人就只能在外面等,等里面的人出去之后,后面的人也就可以進去了,周而復始。
高速緩存行。當CPU從內存中讀取一個字節時,它並不是真的只讀一個字節,而是讀取一個高速緩存行,一個高速緩存行可能是32個字節、64個字節或是128個字節,它始終讀取的字節數是32的整數倍,這樣CPU就不用非常頻繁的讀取內存,從而提高程序的性能,當CPU訪問某塊內存是它會訪問這塊內存旁邊的內存的概率是非常大的,於是就一起讀了。更多關於數據對齊的信息請看我的文章《數據對齊》。
高級線程同步。剛剛簡單的說了一下旋轉鎖,現在又來說旋轉鎖的壞,旋轉鎖的問題在於等待的線程一直在執行毫無用處的該死的死循環,浪費CPU的時間,這肯定是不能容忍的,雖然曾經一度容忍過它。當一個線程需要某個資源,而這個資源被另一個線程占用時,如果這個線程等了一會兒還不能獲得這個資源,那么這個線程就應該被切換到等待狀態,讓系統充當該線程的代理,當該資源可以被使用時,系統就會將該線程喚醒,然后該線程就可以獨占該資源。而實現這一功能的就是關鍵段。
關鍵段。關鍵段是一小段代碼,在執行之前需要獨占對一些共享資源的訪問,這種方式可以讓多行代碼以原子的方式進行訪問,當有一個線程對訪問這段代碼時其他線程只能等待。使用關鍵段的步驟如下:
CRITICAL_SECTION g_cs;//構造一個CRITICAL_SECTION實例
InitializeCriticalSection(&g_cs);//初始化g_cs的成員
EnterCriticalSection(&g_cs);//進入關鍵段
LeaveCriticalSection(&g_cs);//離開關鍵段
DeleteCriticalSection(&g_cs);//清理g_cs
EnterCriticalSection會檢查結構CRITICAL_SECTION的成員變量,這些成員表示是否有線程正在訪問資源,以及哪個線程正在訪問資源,EnterCriticalSection會進行一些測試。如果沒有線程正在訪問資源,EnterCriticalSection會更新變量成員,以表示已經有線程正在訪問資源,並馬上從EnterCriticalSection返回,繼續執行關鍵段中的代碼,如果變量成員表示已經有線程正在訪問資源,那么EnterCriticalSection會使用一個事件內核對象把線程切換成等待狀態,等待狀態的線程是不會浪費CPU的時間的,系統會記住這個線程想要使用這個資源,一旦當前線程調用LeaveCriticalSection,系統會自動更新CRITICAL_SECTION的成員變量,並將等待的線程切換成可調度狀態。
LeaveCriticalSection會檢查結構CRITICAL_SECTION的成員變量並將計數器減一,如果計數器變為0,LeaveCriticalSection會更新成員變量表示現在沒有線程訪問資源,若有等待的線程,則將等待的線程切換成可調度的狀態。
當一個線程進入關鍵段時,若有線程正在訪問關鍵段,那么系統就會將新的線程切換成等待狀態,這意味着將線程從用戶模式切換成內核模式,這個切換的開銷大約是1000個CPU周期,這個開銷其實是很大的,所以在EnterCriticalSection內部使用旋轉鎖,並不是馬上將線程切換成等待狀態,而是先用旋轉鎖試探一些,看線程是否釋放了對資源的訪問,如果釋放了,新的線程就不用被切換成等待狀態了,就可以直接訪問資源了,也就是說花了旋轉鎖輪詢的時間,如果旋轉鎖輪詢了一段時間,線程還是沒有釋放資源,對不起系統就不會讓它繼續輪詢了,因為系統也不知道還要輪詢多久,畢竟輪詢一直都是在消耗CPU的時間,系統會停止輪詢,將新的線程切換成等待狀態,當前一個資源釋放對資源的訪問,系統會將新的線程切換成可調度狀態。
Silm讀/寫鎖。SRWLock的目的和關鍵段是一樣的,就是對資源的保護,不讓其他線程訪問。不同的是,它區分線程是讀線程還是寫線程。我們都是知道,一個資源可以同時被多個線程同時讀,就是不能同時讀,或是讀寫。也是是說寫必須是獨占的方式,而讀可以以共享的方式訪問。
讀寫鎖調用的函數如下,跟關鍵段差不多,我就不廢話了。
RTL_SRWLOCK lock;
InitializeSRWLock(&lock);
AcquireSRWLockExclusive(&lock);//獨占的方式訪問
ReleaseSRWLockExclusive(&lock);
AcquireSRWLockShared(&lock);//共享的方式訪問
ReleaseSRWLockShared(&lock);
作者:陳太漢
博客:http://www.cnblogs.com/hlxs/