1.?? 多線程的那點兒事(基礎篇)
?多線程編程是現代軟件技術中很重要的一個環節。要弄懂多線程,這就要牽涉到多進程?當然,要了解到多進程,就要涉及到操作系統。不過大家也不要緊張,聽我慢慢道來。這其中的環節其實并不復雜。
?? ?(1)單 CPU 下的多線程
?? ? 在沒有出現多核CPU 之前,我們的計算資源是唯一的。如果系統中有多個任務要處理的話,那么就需要按照某種規則依次調度這些任務進行處理。什么規則呢?可以是一些簡單的調度方法,比如說
?? ?1)按照優先級調度
?? ?2)按照FIFO 調度
?? ?3)按照時間片調度等等
?? ?當然,除了CPU 資源之外,系統中還有一些其他的資源需要共享,比如說內存、文件、端口、 socket 等。既然前面說到系統中的資源是有限的,那么獲取這些資源的最小單元體是什么呢,其實就是進程。
?? ?舉個例子來說,在linux 上面每一個享有資源的個體稱為 task_struct ,實際上和我們說的進程是一樣的。我們可以看看 task_struct ( linux?0.11 代碼)都包括哪些內容,
[cpp] view plaincopy
struct ?task_struct?{???? ????long ?state;??? ????long ?counter;?? ????long ?priority;?? ????long ?signal;?? ????struct ?sigaction?sigaction[32];?? ????long ?blocked;????? ?? ????int ?exit_code;?? ????unsigned?long ?start_code,end_code,end_data,brk,start_stack;?? ????long ?pid,father,pgrp,session,leader;?? ????unsigned?short ?uid,euid,suid;?? ????unsigned?short ?gid,egid,sgid;?? ????long ?alarm;?? ????long ?utime,stime,cutime,cstime,start_time;?? ????unsigned?short ?used_math;?? ?? ????int ?tty;?????????? ????unsigned?short ?umask;?? ????struct ?m_inode?*?pwd;?? ????struct ?m_inode?*?root;?? ????struct ?m_inode?*?executable;?? ????unsigned?long ?close_on_exec;?? ????struct ?file?*?filp[NR_OPEN];?? ?? ????struct ?desc_struct?ldt[3];?? ?? ????struct ?tss_struct?tss;?? };??
?? ?每一個task 都有自己的 pid ,在系統中資源的分配都是按照 pid 進行處理的。這也就說明,進程確實是資源分配的主體。
?? ?這時候,可能有朋友會問了,既然task_struct 是資源分配的主體,那為什么又出來 thread ?為什么系統調度的時候是按照 thread調度 ,而不是按照 進程 調度呢?原因其實很簡單, 進程 之間的數據溝通非常麻煩,因為我們之所以把這些 進程 分開,不正是希望它們之間不要相互影響嘛。
?? ?假設是兩個進程 之間數據傳輸,那么需要如果需要對共享數據進行訪問需要哪些步驟呢,
?? ?1)創建共享內存
?? ?2)訪問共享內存-> 系統調用 -> 讀取數據
?? ?3)寫入共享內存-> 系統調用 -> 寫入數據
?? ?要是寫個代碼,大家可能就更明白了,
[cpp] view plaincopy
#include?<unistd.h> ??#include?<stdio.h> ???? int ?value?=?10;???? int ?main(int ?argc,?char *?argv[])??{?? ????int ?pid?=?fork();?? ????if (!pid){?? ????????Value?=?12;?? ????????return ?0;?? ????}?? ????printf("value?=?%d\n" ,?value);?? ????return ?1;?? }??
?? ?上面的代碼是一個創建子進程的代碼,我們發現打印的value 數值還是 10 。盡管中間創建了子進程,修改了 value 的數值,但是我們發現打印下來的數值并沒有發生改變,這就說明了不同的進程之間內存上是不共享的。
?? ?那么,如果修改成thread 有什么好處呢?其實最大的好處就是每個 thread 除了享受單獨 cpu 調度的機會,還能共享每個進程下的所有資源。要是調度的單位是進程,那么每個進程只能干一件事情,但是進程之間是需要相互交互數據的,而進程之間的數據都需要系統調用才能應用,這在無形之中就降低了數據的處理效率。
?? ?(2)多核 CPU 下的多線程
?? ?沒有出現多核之前,我們的CPU 實際上是按照某種規則對線程依次進行調度的。在某一個特定的時刻, CPU 執行的還是某一個特定的線程。然而,現在有了多核 CPU ,一切變得不一樣了,因為在某一時刻很有可能確實是 n 個任務在 n 個核上運行。我們可以編寫一個簡單的 open?mp 測試一下,如果還是一個核,運行的時間就應該是一樣的。
[cpp] view plaincopy
#include?<omp.h> ??#define?MAX_VALUE?10000000 ???? double ?_test(int ?value)??{?? ????int ?index;?? ????double ?result;?? ?? ????result?=?0.0;?? ????for (index?=?value?+?1;?index?<?MAX_VALUE;?index?+=2?)?? ????????result?+=?1.0?/?index;?? ?? ????return ?result;?? }?? ?? void ?test()??{?? ????int ?index;?? ????int ?time1;?? ????int ?time2;?? ????double ?value1,value2;?? ????double ?result[2];?? ?? ????time1?=?0;?? ????time2?=?0;?? ?? ????value1?=?0.0;?? ????time1?=?GetTickCount();?? ????for (index?=?1;?index?<?MAX_VALUE;?index?++)?? ????????value1?+=?1.0?/?index;?? ?? ????time1?=?GetTickCount()?-?time1;?? ?? ????value2?=?0.0;?? ????memset(result?,?0,?sizeof (double )?*?2);?? ????time2?=?GetTickCount();?? ?? #pragma?omp?parallel?for ??????for (index?=?0;?index?<?2;?index++)?? ????????result[index]?=?_test(index);?? ?? ????value2?=?result[0]?+?result[1];?? ????time2?=?GetTickCount()?-?time2;?? ?? ????printf("time1?=?%d,time2?=?%d\n" ,time1,time2);?? ????return ;?? }??
?? ?(3)多線程編程
為什么要多線程編程呢?這其中的原因很多,我們可以舉例解決
?? ?1)有的是為了提高運行的速度,比如多核cpu 下的多線程
?? ?2)有的是為了提高資源 的利用率,比如在網絡環境下下載資源時,時延常常很高,我們可以通過不同的thread從不同的地方獲取資源,這樣可以提高效率
?? ?3)有的為了提供更好的服務,比如說是服務器
?? ?4)其他需要多線程編程的地方等等
2. 多線程的那點兒事(之數據同步)
多線程創建其實十分簡單,在windows 系統下面有很多函數可以創建多線程,比如說 _beginthread 。我們就可以利用它為我們編寫一段簡單的多線程代碼,
[cpp] view plaincopy
#include?<windows.h> ??#include?<process.h> ??#include?<stdio.h> ???? unsigned?int ?value?=?0;?? ?? void ?print(void *?argv)??{?? ????while (1){?? ????????printf("&value?=?%x,?value?=?%d\n" ,?&value,?value);?? ????????value?++;?? ????????Sleep(1000);?? ????}?? }?? ?? int ?main()??{?? ????_beginthread(?print,?0,?NULL?);?? ????_beginthread(?print,?0,?NULL);?? ?? ????while (1)??? ????????Sleep(0);?? ?? ????return ?1;?? }??
?? ?注意,在VC 上面編譯的時候,需要打開 /MD 開關。具體操作為,【 project 】 -> 【 setting 】 -> 【 c/c++ 】 ->Category 【 Code?Generation 】 -> 【 Use?run-time?library 】 -> 【 Debug?Multithreaded 】即可。
?? ?通過上面的示例,我們看到作為共享變量的value 事實上是可以被所有的線程訪問的。這就是線程數據同步的最大優勢——方便,直接。因為線程之間除了堆棧空間不一樣之外,代碼段和數據段都是在一個空間里面的。所以,線程想訪問公共數據,就可以訪問公共數據,沒有任何的限制。
?? ?當然,事物都有其兩面性。這種對公共資源的訪問模式也會導致一些問題。什么問題呢?我們看了就知道了。
?? ?現在假設有一個池塘,我們雇兩個人來喂魚。兩個人不停地對池塘里面的魚進行喂食。我們規定在一個人喂魚的時候,另外一個人不需要再喂魚,否則魚一次喂兩回就要撐死了。為此,我們安裝了一個牌子作為警示。如果一個人在喂魚,他會把牌子設置為FALSE ,那么另外一個人看到這個牌子,就不會繼續喂魚了。等到這個人喂完后,他再把牌子繼續設置為 TRUE 。
?? ?如果我們需要把這個故事寫成代碼,那么怎么寫呢?朋友們試試看,
[cpp] view plaincopy
while (1){??????if (?flag?==?true ){?? ????????flag?=?false ;?? ????????do_give_fish_food();?? ????????flag?=?true ;?? ????}?? ?? ????Sleep(0);?? }??
?? ?上面的代碼看上去沒有問題了,但是大家看看代碼的匯編代碼,看看是不是存在隱患。因為還會出現兩個人同時喂食的情況,
[cpp] view plaincopy
23:???????while (1){?? 004010E8???mov?????????eax,1?? 004010ED???test????????eax,eax?? 004010EF???je??????????do_action+56h?(00401126)?? 24:???????????if (?flag?==?true ){?? 004010F1???cmp?????????dword?ptr?[flag?(00433e04)],1?? 004010F8???jne?????????do_action+43h?(00401113)?? 25:???????????????flag?=?false ;?? 004010FA???mov?????????dword?ptr?[flag?(00433e04)],0?? 26:???????????????do_give_fish_food();?? 00401104???call????????@ILT+15(do_give_fish_food)?(00401014)?? 27:???????????????flag?=?true ;?? 00401109???mov?????????dword?ptr?[flag?(00433e04)],1?? 28:???????????}?? 29:?? 30:???????????Sleep(0);?? 00401113???mov?????????esi,esp?? 00401115???push????????0?? 00401117???call????????dword?ptr?[__imp__Sleep@4?(004361c4)]?? 0040111D???cmp?????????esi,esp?? 0040111F???call????????__chkesp?(004011e0)?? 31:???????}?? 00401124???jmp?????????do_action+18h?(004010e8)?? 32:???}??
?? ?我們此時假設有兩個線程a 和 b 在不停地進行判斷和喂食操作。設置當前 flag?=?true ,此時線程 a 執行到 004010F8 處時,判斷魚還沒有喂食,正準備執行指令 004010F8 ,但是還沒有來得及對 falg 進行設置,此時出現了線程調度。線程 b 運行到 004010F8 時,發現當前沒有人喂食,所以執行喂食操作。等到 b 線程喂食結束,運行到 00401113 的時候,此時又出現了調度。線程 a 有繼續運行,因為之前已經判斷了當前還沒有喂食,所以線程 a 繼續進行了喂食了操作。所以,可憐的魚,這一次就連續經歷了兩次喂食操作,估計有一部分魚要撐死了。
?? ?當然魚在這里之所以會出現撐死的情況,主要是因為line?24 和 line?25 之間出現了系統調度。所以,我們在編寫程序的時候必須有一個牢固的思想意識,如果缺少必須要的手段,程序可以任何時刻任何地點被調度 ,那此時公共數據的計算就會出現錯誤。
?? ?那么有沒有方法避免這種情況的發生呢?當然有。朋友們可以繼續關注下面的博客。
3.? 多線程的那點事兒(之數據互斥)
? 在多線程存在的環境中,除了堆棧中的臨時數據之外,所有的數據都是共享的。如果我們需要線程之間正確地運行,那么務必需要保證公共數據的執行和計算是正確的。簡單一點說,就是保證數據在執行的時候必須是互斥的。否則,如果兩個或者多個線程在同一時刻對數據進行了操作,那么后果是不可想象的。
??? 也許有的朋友會說,不光數據需要保護,代碼也需要保護。提出這個觀點的朋友只看到了數據訪問互斥的表象。在程序的運行空間里面,什么最重要的呢?代碼嗎?當然不是。代碼只是為了數據的訪問存在的。數據才是我們一切工作的出發點和落腳點。
??? 那么,有什么辦法可以保證在某一時刻只有一個線程對數據進行操作呢?四個基本方法:
??? (1)關中斷
??? (2)數學互斥方法
??? (3)操作系統提供的互斥方法
??? (4)cpu原子操作
??? 為了讓大家可以對這四種方法有詳細的認識,我們可以進行詳細的介紹。
??
??? (1)關中斷
??? 要讓數據在某一時刻只被一個線程訪問,方法之一就是停止線程調度就可以了。那么怎樣停止線程調度呢?那么關掉時鐘中斷就可以了啊。在X86里面的確存在這樣的兩個指令,
[cpp] view plaincopy
#include?<stdio.h> ???? int ?main()??{?? ????__asm{?? ????????cli?? ????????sti?? ????}?? ????return ?1;?? }??
??? 其中cli是關中斷,sti是開中斷。這段代碼沒有什么問題,可以編過,當然也可以生成執行文件。但是在執行的時候會出現一個異常告警:Unhandled exception in test.exe: 0xC0000096:? Privileged Instruction。告警已經說的很清楚了,這是一個特權指令。只有系統或者內核本身才可以使用這個指令。
??? 不過,大家也可以想象一下。因為平常我們編寫的程序都是應用級別的程序,要是每個程序都是用這些代碼,那不亂了套了。比如說,你不小心安裝一個低質量的軟件,說不定什么時候把你的中斷關了,這樣你的網絡就斷了,你的輸入就沒有回應了,你的音樂什么都沒有了,這樣的環境你受的了嗎?應用層的軟件是千差萬別的,軟件的水平也是參差不齊的,所以系統不可能相信任何一個私有軟件,它相信的只是它自己。
?
??? (2)數學方法
??? 假設有兩個線程(a、b)正要對一個共享數據進行訪問,那么怎么做到他們之間的互斥的呢?其實我們可以這么做,
[cpp] view plaincopy
unsigned?int ?flag[2]?=?{0};?? unsigned?int ?turn?=?0;?? ?? void ?process(unsigned?int ?index)??{?? ????flag[index]?=?1;?? ????turn?=??index;?? ?? ????while (flag[1?-?index]?&&?(turn?==??index));?? ????do_something();?? ????flag[index]?=?0;?? }??
??? 其實,學過操作系統的朋友都知道,上面的算法其實就是Peterson算法,可惜它只能用于兩個線程的數據互斥。當然,這個算法還可以推廣到更多線程之間的互斥,那就是bakery算法。但是數學算法有兩個缺點:
??? a)占有空間多,兩個線程就要flag占兩個單位空間,那么n個線程就要n個flag空間,
??? b)代碼編寫復雜,考慮的情況比較復雜
?
??? (3)系統提供的互斥算法
??? 系統提供的互斥算法其實是我們平時開發中用的最多的互斥工具。就拿windows來說,關于互斥的工具就有臨界區、互斥量、信號量等等。這類算法有一個特點,那就是都是依據系統提高的互斥資源,那么系統又是怎么完成這些功能的呢?其實也不難。
??? 系統加鎖過程,
[cpp] view plaincopy
void ?Lock(HANDLE ?hLock)??{?? ????__asm?{cli};?? ?? ????while (1){?? ????????if (){?? ?????????????? ????????????__asm?{sti};?? ????????????return ;?? ????????}?? ?? ????????__asm{sti};?? ????????schedule();?? ????????__asm{cli};?? ????}?? }??
??? 系統解鎖過程,
[cpp] view plaincopy
void ?UnLock(HANDLE ?hLock)??{?? ????__asm?{cli};?? ?????? ????__asm{sti};?? }??
??? 上面其實討論的就是一種最簡單的系統鎖情況。中間沒有涉及到就緒線程的壓入和彈出過程,沒有涉及到資源個數的問題,所以不是很復雜。朋友們仔細看看,應該都可以明白代碼表達的是什么意思。
?
??? (4)CPU的原子操作 ??? 因為在多線程操作當中,有很大一部分是比較、自增、自減等簡單操作。因為需要互斥的代碼很少,所以使用互斥量、信號量并不合算。因此,CPU廠商為了開發的方便,把一些常用的指令設計成了原子指令,在windows上面也被稱為原子鎖,常用的原子操作函數有
[cpp] view plaincopy
InterLockedAdd?? ?? InterLockedExchange?? ?? InterLockedCompareExchange?? ?? InterLockedIncrement?? ?? InterLockedDecrement?? ?? InterLockedAnd?? ?? InterLockedOr?
4.
多線程的那點兒事(之自旋鎖) ?自旋鎖是SMP中經常使用到的一個鎖。所謂的smp,就是對稱多處理器的意思。在工業用的pcb板上面,特別是服務器上面,一個pcb板有多個cpu是很正常的事情。這些cpu相互之間是獨立運行的,每一個cpu均有自己的調度隊列。然而,這些cpu在內存空間上是共享的。舉個例子說,假設有一個數據value = 10,那么這個數據可以被所有的cpu訪問。這就是共享內存的本質意義。
?? ?我們可以看一段Linux 下的的自旋鎖代碼(kernel 2.6.23,asm-i386/spinlock.h),就可有清晰的認識了,
[cpp] view plaincopy
static ?inline ?void ?__raw_spin_lock(raw_spinlock_t?*lock)??{?? ????asm?volatile ("\n1:\t" ?? ?????????????LOCK_PREFIX?"?;?decb?%0\n\t" ?? ?????????????"jns?3f\n" ?? ?????????????"2:\t" ?? ?????????????"rep;nop\n\t" ?? ?????????????"cmpb?$0,%0\n\t" ?? ?????????????"jle?2b\n\t" ?? ?????????????"jmp?1b\n" ?? ?????????????"3:\n\t" ?? ?????????????:?"+m" ?(lock->slock)?:?:?"memory" );?? }??
上面這段代碼是怎么做到自旋鎖的呢?我們可以一句一句看看,
line ?4: 對lock->slock自減,這個操作是互斥的,LOCK_PREFIX保證了此刻只能有一個CPU訪問內存
line ?5: 判斷lock->slock是否為非負數,如果是跳轉到3,即獲得自旋鎖
line ?6: 位置符
line ?7: lock->slock此時為負數,說明已經被其他cpu搶占了,cpu休息一會,相當于pause指令
line ?8: 繼續將lock->slock和0比較,
line ?9: 判斷lock->slock是否小于等于0,如果判斷為真,跳轉到2,繼續休息
line 10: 此時lock->slock已經大于0,可以繼續嘗試搶占了,跳轉到1
line 11: 位置符?
??
?? ?上面的操作,除了第4句是cpu互斥操作,其他都不是。所以,我們發現,在cpu之間尋求互斥訪問的時候,在某一時刻只有一個內存訪問權限。所以,如果其他的cpu之間沒有獲得訪問權限,就會不斷地查看當前是否可以再次申請自旋鎖了。這個過程中間不會停歇,除非獲得訪問的權限為止。
總結:
?? 1)在smp上自旋鎖是多cpu互斥訪問的基礎
?? 2)因為自旋鎖是自旋等待的,所以處于臨界區的代碼應盡可能短
?? 3)上面的LOCK_PREFIX,在x86下面其實就是“lock”,gcc下可以編過,朋友們可以自己試試
5.
多線程的那點兒事(之windows鎖) 在windows系統中,系統本身為我們提供了很多鎖。通過這些鎖的使用,一方面可以加強我們對鎖的認識,另外一方面可以提高代碼的性能和健壯性。常用的鎖以下四種:臨界區,互斥量,信號量,event。
?? ?
?? ?(1)臨界區
?? ?臨界區是最簡單的一種鎖。基本的臨界區操作有,
[cpp] view plaincopy
InitializeCriticalSection?? EnterCriticalSection?? LeaveCriticalSection?? DeleteCriticalSection??
?? ?如果想要對數據進行互斥操作的話,也很簡單,這樣做就可以了,
[cpp] view plaincopy
EnterCriticalSection()?? ????do_something();?? LeaveCriticalSection()??
?? ?
(2)互斥鎖
?? ?互斥鎖也是一種鎖。和臨界區不同的是,它可以被不同進程使用,因為它有名字。同時,獲取鎖和釋放鎖的線程必須是同一個線程。常用的互斥鎖操作有
[cpp] view plaincopy
CreateMutex?? OpenMutex?? ReleaseMutex??
?? ?那么,怎么用互斥鎖進行數據的訪問呢,其實不難。
[cpp] view plaincopy
WaitForSingleObject();?? ????do_something();?? ReleaseMutex();??
?? ?
(3)信號量
?? ?信號量是使用的最多的一種鎖結果,也是最方便的一種鎖。圍繞著信號量,人們提出了很多數據互斥訪問的方案,pv操作就是其中的一種。如果說互斥鎖只能對單個資源進行保護,那么信號量可以對多個資源進行保護。同時信號量在解鎖的時候,可以被另外一個thread進行解鎖操作。目前,常用的信號量操作有,
[cpp] view plaincopy
CreateSemaphore?? OpenSemaphore?? ReleaseSemaphore??
?? ?信號量的使用和互斥鎖差不多。關鍵是信號量在初始化的時候需要明確當前資源的數量和信號量的初始狀態是什么,
[cpp] view plaincopy
WaitForSingleObject();?? ????do_something();?? ReleaseSemaphore();??
?? ?
(4)event對象
?? ?event對象是windows下面很有趣的一種鎖結果。從某種意義上說,它和互斥鎖很相近,但是又不一樣。因為在thread獲得鎖的使用權之前,常常需要main線程調用SetEvent設置一把才可以。關鍵是,在thread結束之前,我們也不清楚當前thread獲得event之后執行到哪了。所以使用起來,要特別小心。常用的event操作有,
[cpp] view plaincopy
CreateEvent?? OpenEvent?? PulseEvent?? ResetEvent?? SetEvent??
?? ?我們對event的使用習慣于分成main thread和normal thread使用。main thread負責event的設置和操作,而normal thread負責event的等待操作。在CreateEvent的時候,要務必考慮清楚event的初始狀態和基本屬性。
?? ?對于main thread,應該這么做,
[cpp] view plaincopy
CreateEvent();?? SetEvent();?? WaitForMultiObjects(hThread,?);?? CloseHandle();??
?? ?對于normal thread來說,操作比較簡單,
[cpp] view plaincopy
while (1){??????WaitForSingleObject();?? ?? ?????? }??
總結:
?? ?(1)關于
臨界區、
互斥區、
信號量、
event在msdn上均有示例代碼
?? ?(2)一般來說,使用頻率上信號量 > 互斥區 > 臨界區 > 事件對象
?? ?(3)信號量可以實現其他三種鎖的功能,學習上應有所側重
?? ?(4)紙上得來終覺淺,多實踐才能掌握它們之間的區別
6.
多線程的那點兒事(之C++鎖) ? 編寫程序不容易,編寫多線程的程序更不容易。相信編寫過多線程的程序都應該有這樣的一個痛苦過程,什么樣的情況呢?朋友們應該看一下代碼就明白了,
[cpp] view plaincopy
void ?data_process()??{?? ????EnterCriticalSection();?? ???? ????if ()?? ????{?? ????????LeaveCriticalSection();?? ????????return ;?? ????}?? ?? ????if ()?? ????{?? ????????return ;?? ????}?? ?? ????LeaveCriticalSection();?? }??
?? ?上面的代碼說明了一種情形。這種多線程的互斥情況在代碼編寫過程中是經常遇到的。所以,每次對共享數據進行操作時,都需要對數據進行EnterCriticalSection和LeaveCriticalSection的操作。但是,這中間也不是一帆風順的。很有可能你會遇到各種各樣的錯誤。那么,這時候你的程序就需要跳出去了。可能一開始遇到error的時候,你還記得需要退出臨界區。但是,如果錯誤多了,你未必記得還有這個操作了。這一錯就完了,別的線程就沒有機會獲取這個鎖了。
?? ?那么,有沒有可能利用C++的特性,自動處理這種情況呢?還真有。我們看看下面這個代碼,
[cpp] view plaincopy
class ?CLock??{?? ????CRITICAL_SECTION&?cs;?? ?? public :??????CLock(CRITICAL_SECTION&?lock):cs(lock){?? ????????EnterCriticalSection(&cs);?? ????}?? ?? ????~CLock()?{?? ????????LeaveCriticalSection(&cs);?? ????}?? }?? ?? class ?Process??{?? ????CRITICAL_SECTION?cs;?? ?????? ?? public :??????Process(){?? ????????InitializeCriticalSection(&cs);?? ????}?? ?? ????~Process()?{DeleteCriticalSection(&cs);}?? ?? ????void ?data_process(){?? ????????CLock?lock(cs);?? ?? ????????if (){?? ????????????return ;?? ????????}?? ?? ????????return ;?? ????}?? }??
?? ?C++的一個重要特點就是,不管函數什么時候退出,系統都會自動調用類的析構函數。在Process類的data_process函數中,,函數在開始就創建了一個CLock類。那么,在創建這個類的時候,其實就開始了臨界區的pk。那么一旦進入到臨界區當中,在error中能不能及時退出臨界區呢?此時,c++析構函數的優勢出現了。因為不管錯誤什么時候出現,在函數退出之前,系統都會幫我們善后。什么善后呢?就是系統會調用CLock的析構函數,也就是退出臨界區。這樣,我們的目的就達到了。
?? ?其實,這就是一個c++的trick。
7.
多線程的那點兒事(之原子鎖) ?原子鎖是多線程編程中的一個特色。然而,在平時的軟件編寫中,原子鎖的使用并不是很多。這其中原因很多,我想主要有兩個方面。第一,關于原子鎖這方面的內容介紹的比較少;第二,人們在編程上面習慣于已有的方案,如果沒有特別的需求,不過貿然修改已存在的代碼。畢竟對很多人來說,不求有功,但求無過。保持當前代碼的穩定性還是很重要的。 ?
?? ?其實,早在《
多線程數據互斥》這篇博客中,我們就已經介紹過原子鎖。本篇博客主要討論的就是原子鎖怎么使用。中間的一些用法只是我個人的一些經驗,希望能夠拋磚引玉,多聽聽大家的想法。
?? ?(1)查找函數中原子鎖?? ?
?? ?在一些函數當中,有的時候我們需要對滿足某種特性的數據進行查找。在傳統的單核CPU上,優化的空間比較有限。但是,現在多核CPU已經成了主流配置。所以我們完全可以把這些查找工作分成幾個子函數分在幾個核上面并行運算。但是,這中間就會涉及到一個問題,那就是對公共數據的訪問。傳統的訪問方式,應該是這樣的,
[cpp] view plaincopy
unsigned?int ?count?=?0;?? ?? int ?find_data_process()??{?? ????if (){?? ?????????EnterCriticalSection(&cs);?? ?????????count?++;?? ?????????LeaveCriticalSection(&cs);??????????? ????}?? }??
?? ?我們看到代碼中間使用到了鎖,那么勢必會涉及到系統調用和函數調度。所以,在執行效率上會大打折扣。那么如果使用原子鎖呢?
[cpp] view plaincopy
unsigned?int ?count?=?0;?? ?? int ?find_data_process()??{?? ????if (){?? ????????InterLockedIncrement(&count);?? ????}?? }??
?? ?有興趣的朋友可以做這樣一道題目,查看0~0xFFFFFFFF上有多少數可以被3整除?大家也可以驗證一下用原子鎖代替臨界區之后,代碼的效率究竟可以提高多少。關于多核多線程的編程,朋友們可以參考《多線程基礎篇》這篇博客。
?? ?(2)代碼段中的原子鎖 ?? ?上面的范例只是介紹了統計功能中的原子鎖。那么怎么用原子鎖代替傳統的系統鎖呢?比如說,假設原來的數據訪問是這樣的,
[cpp] view plaincopy
void ?data_process()??{?? ????EnterCriticalSection(&cs);?? ????do_something();?? ????LeaveCriticalSection(&cs);????? }??
?? ?如果改成原子鎖呢,會是什么樣的呢?
[cpp] view plaincopy
unsigned?int ?lock?=?0;?? ?? void ?data_process()??{?? ????while (1?==?InterLockedCompareExchange(&lock,?1,?0));?? ????do_something();?? ????lock?=?0;?????? }??
?? ?這里用原子鎖代替普通的系統鎖,完成的功能其實是一樣的。那么這中間有什么區別呢?其實,關鍵要看do_something要執行多久。打個比方來說,現在我們去買包子,但是買包子的人很多。那怎么辦呢?有兩個選擇,如果賣包子的人手腳麻利,服務一個顧客只要10秒鐘,那么即使前面排隊的有50個人,我們只要等7、8分鐘就可以,這點等的時間還是值得的;但是如果不幸這個賣包子的老板服務一個顧客要1分鐘,那就悲催了,假使前面有50個人,那我們就要等50多分鐘了。50分鐘對我們來說可是不短的一個時間,我們完全可以利用這個時間去買點水果,交交水電費什么的,過了這個時間點再來買包子也不遲。
?? ?和上面的例子一樣,忙等的方法就是原子鎖,過一會再來的方法就是哪個傳統的系統鎖。用哪個,就看這個do_something的時間值不值得我們等待了。
8.
多線程的那點兒事(之讀寫鎖) 在編寫多線程的時候,有一種情況是十分常見的。那就是,有些公共數據修改的機會比較少。相比較改寫,它們讀的機會反而高的多。通常而言,在讀的過程中,往往伴隨著查找的操作,中間耗時很長。給這種代碼段加鎖,會極大地降低我們程序的效率。那么有沒有一種方法,可以專門處理這種多讀少寫的情況呢?
?? ?有,那就是讀寫鎖。
?? ?(1)首先,我們定義一下基本的數據結構。
[cpp] view plaincopy
typedef ?struct ?_RWLock??{?? ????int ?count;?? ????int ?state;?? ????HANDLE ?hRead;?? ????HANDLE ?hWrite;?? }RWLock;?????
?? ?同時,為了判斷當前的鎖是處于讀狀態,還是寫狀態,我們要定義一個枚舉量,
[cpp] view plaincopy
typedef ?enum ??{?? ????STATE_EMPTY?=?0,?? ????STATE_READ,?? ????STATE_WRITE?? };??
?? ?
(2)初始化數據結構
[cpp] view plaincopy
RWLock*?create_read_write_lock(HANDLE ?hRead,?HANDLE ?hWrite)?? {?? ????RWLock*?pRwLock?=?NULL;?? ?? ????assert(NULL?!=?hRead?&&?NULL?!=?hWrite);?? ????pRwLock?=?(RWLock*)malloc(sizeof (RWLock));?? ???? ????pRwLock->hRead?=?hRead;?? ????pRwLock->hWrite?=?hWrite;?? ????pRwLock->count?=?0;?? ????pRwLock->state?=?STATE_EMPTY;?? ????return ?pRwLock;?? }??
?? ?
(3)獲取讀鎖
[cpp] view plaincopy
void ?read_lock(RWLock*?pRwLock)??{?? ????assert(NULL?!=?pRwLock);?? ?????? ????WaitForSingleObject(pRwLock->hRead,?INFINITE);?? ????pRwLock->counnt?++;?? ????if (1?==?pRwLock->count){?? ????????WaitForSingleObject(pRwLock->hWrite,?INFINITE);?? ????????pRwLock->state?=?STATE_READ;?? ????}?? ????ReleaseMutex(pRwLock->hRead);?? }??
?? ?
(4)獲取寫鎖
[cpp] view plaincopy
void ?write_lock(RWLock*?pRwLock)??{?? ????assert(NULL?!=?pRwLock);?? ?? ????WaitForSingleObject(pRwLock->hWrite,?INFINITE);?? ????pRwLock->state?=?STATE_WRITE;?? }??
?? ?
(5)釋放讀寫鎖
[cpp] view plaincopy
void ?read_write_unlock(RWLock*?pRwLock)??{?? ????assert(NULL?!=?pRwLock);?? ?? ????if (STATE_READ?==?pRwLock->state){?? ????????WaitForSingleObject(pRwLock->hRead,?INFINITE);?? ????????pRwLock->count?--;?? ????????if (0?==?pRwLock->count){?? ????????????pRwLock->state?=?STATE_EMPTY;?? ????????????ReleaseMutex(pRwLock->hWrite);?? ????????}?? ????????ReleaseMutex(pRwLock->hRead);?? ????}else {?? ????????pRwLock->state?=?STATE_EMPTY;?? ????????ReleaseMutex(pRwLock->hWrite);?? ????}?? ?????? ????return ;?? }??
文章總結:
?? ?(1)讀寫鎖的優勢只有在多讀少寫、代碼段運行時間長這兩個條件下才會效率達到最大化;
?? ?(2)任何公共數據的修改都必須在鎖里面完成;
?? ?(3)讀寫鎖有自己的應用場所,選擇合適的應用環境十分重要;
?? ?(4)編寫讀寫鎖很容易出錯,朋友們應該多加練習;
?? ?(5)讀鎖和寫鎖一定要分開使用,否則達不到效果。
總結
以上是生活随笔 為你收集整理的多线程的那点儿事 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。