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

作者:陳太漢
浙公網(wǎng)安備 33010602011771號(hào)