多線程的那點兒事

1.?? 多線程的那點兒事(基礎篇)


?多線程編程是現代軟件技術中很重要的一個環節。要弄懂多線程,這就要牽涉到多進程?當然,要了解到多進程,就要涉及到操作系統。不過大家也不要緊張,聽我慢慢道來。這其中的環節其實并不復雜。

?? ?(1)單CPU下的多線程

?? ? 在沒有出現多核CPU之前,我們的計算資源是唯一的。如果系統中有多個任務要處理的話,那么就需要按照某種規則依次調度這些任務進行處理。什么規則呢?可以是一些簡單的調度方法,比如說

?? ?1)按照優先級調度

?? ?2)按照FIFO調度

?? ?3)按照時間片調度等等

?? ?當然,除了CPU資源之外,系統中還有一些其他的資源需要共享,比如說內存、文件、端口、socket等。既然前面說到系統中的資源是有限的,那么獲取這些資源的最小單元體是什么呢,其實就是進程。

?? ?舉個例子來說,在linux上面每一個享有資源的個體稱為task_struct,實際上和我們說的進程是一樣的。我們可以看看task_structlinux?0.11代碼)都包括哪些內容,

[cpp] view plaincopy
  1. struct?task_struct?{??
  2. /*?these?are?hardcoded?-?don't?touch?*/??
  3. ????long?state;?/*?-1?unrunnable,?0?runnable,?>0?stopped?*/??
  4. ????long?counter;??
  5. ????long?priority;??
  6. ????long?signal;??
  7. ????struct?sigaction?sigaction[32];??
  8. ????long?blocked;???/*?bitmap?of?masked?signals?*/??
  9. /*?various?fields?*/??
  10. ????int?exit_code;??
  11. ????unsigned?long?start_code,end_code,end_data,brk,start_stack;??
  12. ????long?pid,father,pgrp,session,leader;??
  13. ????unsigned?short?uid,euid,suid;??
  14. ????unsigned?short?gid,egid,sgid;??
  15. ????long?alarm;??
  16. ????long?utime,stime,cutime,cstime,start_time;??
  17. ????unsigned?short?used_math;??
  18. /*?file?system?info?*/??
  19. ????int?tty;????????/*?-1?if?no?tty,?so?it?must?be?signed?*/??
  20. ????unsigned?short?umask;??
  21. ????struct?m_inode?*?pwd;??
  22. ????struct?m_inode?*?root;??
  23. ????struct?m_inode?*?executable;??
  24. ????unsigned?long?close_on_exec;??
  25. ????struct?file?*?filp[NR_OPEN];??
  26. /*?ldt?for?this?task?0?-?zero?1?-?cs?2?-?ds&ss?*/??
  27. ????struct?desc_struct?ldt[3];??
  28. /*?tss?for?this?task?*/??
  29. ????struct?tss_struct?tss;??
  30. };??

?? ?每一個task都有自己的pid,在系統中資源的分配都是按照pid進行處理的。這也就說明,進程確實是資源分配的主體。

?? ?這時候,可能有朋友會問了,既然task_struct是資源分配的主體,那為什么又出來thread?為什么系統調度的時候是按照thread調度,而不是按照進程調度呢?原因其實很簡單,進程之間的數據溝通非常麻煩,因為我們之所以把這些進程分開,不正是希望它們之間不要相互影響嘛。

?? ?假設是兩個進程之間數據傳輸,那么需要如果需要對共享數據進行訪問需要哪些步驟呢,

?? ?1)創建共享內存

?? ?2)訪問共享內存->系統調用->讀取數據

?? ?3)寫入共享內存->系統調用->寫入數據

?? ?要是寫個代碼,大家可能就更明白了,

[cpp] view plaincopy
  1. #include?<unistd.h>??
  2. #include?<stdio.h>??
  3. ??
  4. int?value?=?10;??
  5. ??
  6. int?main(int?argc,?char*?argv[])??
  7. {??
  8. ????int?pid?=?fork();??
  9. ????if(!pid){??
  10. ????????Value?=?12;??
  11. ????????return?0;??
  12. ????}??
  13. ????printf("value?=?%d\n",?value);??
  14. ????return?1;??
  15. }??

?? ?上面的代碼是一個創建子進程的代碼,我們發現打印的value數值還是10。盡管中間創建了子進程,修改了value的數值,但是我們發現打印下來的數值并沒有發生改變,這就說明了不同的進程之間內存上是不共享的。

?? ?那么,如果修改成thread有什么好處呢?其實最大的好處就是每個thread除了享受單獨cpu調度的機會,還能共享每個進程下的所有資源。要是調度的單位是進程,那么每個進程只能干一件事情,但是進程之間是需要相互交互數據的,而進程之間的數據都需要系統調用才能應用,這在無形之中就降低了數據的處理效率。


?? ?(2)多核CPU下的多線程

?? ?沒有出現多核之前,我們的CPU實際上是按照某種規則對線程依次進行調度的。在某一個特定的時刻,CPU執行的還是某一個特定的線程。然而,現在有了多核CPU,一切變得不一樣了,因為在某一時刻很有可能確實是n個任務在n個核上運行。我們可以編寫一個簡單的open?mp測試一下,如果還是一個核,運行的時間就應該是一樣的。

[cpp] view plaincopy
  1. #include?<omp.h>??
  2. #define?MAX_VALUE?10000000??
  3. ??
  4. double?_test(int?value)??
  5. {??
  6. ????int?index;??
  7. ????double?result;??
  8. ??
  9. ????result?=?0.0;??
  10. ????for(index?=?value?+?1;?index?<?MAX_VALUE;?index?+=2?)??
  11. ????????result?+=?1.0?/?index;??
  12. ??
  13. ????return?result;??
  14. }??
  15. ??
  16. void?test()??
  17. {??
  18. ????int?index;??
  19. ????int?time1;??
  20. ????int?time2;??
  21. ????double?value1,value2;??
  22. ????double?result[2];??
  23. ??
  24. ????time1?=?0;??
  25. ????time2?=?0;??
  26. ??
  27. ????value1?=?0.0;??
  28. ????time1?=?GetTickCount();??
  29. ????for(index?=?1;?index?<?MAX_VALUE;?index?++)??
  30. ????????value1?+=?1.0?/?index;??
  31. ??
  32. ????time1?=?GetTickCount()?-?time1;??
  33. ??
  34. ????value2?=?0.0;??
  35. ????memset(result?,?0,?sizeof(double)?*?2);??
  36. ????time2?=?GetTickCount();??
  37. ??
  38. #pragma?omp?parallel?for??
  39. ????for(index?=?0;?index?<?2;?index++)??
  40. ????????result[index]?=?_test(index);??
  41. ??
  42. ????value2?=?result[0]?+?result[1];??
  43. ????time2?=?GetTickCount()?-?time2;??
  44. ??
  45. ????printf("time1?=?%d,time2?=?%d\n",time1,time2);??
  46. ????return;??
  47. }??

?? ?(3)多線程編程

為什么要多線程編程呢?這其中的原因很多,我們可以舉例解決

?? ?1)有的是為了提高運行的速度,比如多核cpu下的多線程

?? ?2)有的是為了提高資源的利用率,比如在網絡環境下下載資源時,時延常常很高,我們可以通過不同的thread從不同的地方獲取資源,這樣可以提高效率

?? ?3)有的為了提供更好的服務,比如說是服務器

?? ?4)其他需要多線程編程的地方等等


2. 多線程的那點兒事(之數據同步)

多線程創建其實十分簡單,在windows系統下面有很多函數可以創建多線程,比如說_beginthread。我們就可以利用它為我們編寫一段簡單的多線程代碼,

[cpp] view plaincopy
  1. #include?<windows.h>??
  2. #include?<process.h>??
  3. #include?<stdio.h>??
  4. ??
  5. unsigned?int?value?=?0;??
  6. ??
  7. void?print(void*?argv)??
  8. {??
  9. ????while(1){??
  10. ????????printf("&value?=?%x,?value?=?%d\n",?&value,?value);??
  11. ????????value?++;??
  12. ????????Sleep(1000);??
  13. ????}??
  14. }??
  15. ??
  16. int?main()??
  17. {??
  18. ????_beginthread(?print,?0,?NULL?);??
  19. ????_beginthread(?print,?0,?NULL);??
  20. ??
  21. ????while(1)???
  22. ????????Sleep(0);??
  23. ??
  24. ????return?1;??
  25. }??

?? ?注意,在VC上面編譯的時候,需要打開/MD開關。具體操作為,【project->setting->c/c++->CategoryCode?Generation->Use?run-time?library->Debug?Multithreaded】即可。

?? ?通過上面的示例,我們看到作為共享變量的value事實上是可以被所有的線程訪問的。這就是線程數據同步的最大優勢——方便,直接。因為線程之間除了堆棧空間不一樣之外,代碼段和數據段都是在一個空間里面的。所以,線程想訪問公共數據,就可以訪問公共數據,沒有任何的限制。

?? ?當然,事物都有其兩面性。這種對公共資源的訪問模式也會導致一些問題。什么問題呢?我們看了就知道了。

?? ?現在假設有一個池塘,我們雇兩個人來喂魚。兩個人不停地對池塘里面的魚進行喂食。我們規定在一個人喂魚的時候,另外一個人不需要再喂魚,否則魚一次喂兩回就要撐死了。為此,我們安裝了一個牌子作為警示。如果一個人在喂魚,他會把牌子設置為FALSE,那么另外一個人看到這個牌子,就不會繼續喂魚了。等到這個人喂完后,他再把牌子繼續設置為TRUE

?? ?如果我們需要把這個故事寫成代碼,那么怎么寫呢?朋友們試試看,

[cpp] view plaincopy
  1. while(1){??
  2. ????if(?flag?==?true){??
  3. ????????flag?=?false;??
  4. ????????do_give_fish_food();??
  5. ????????flag?=?true;??
  6. ????}??
  7. ??
  8. ????Sleep(0);??
  9. }??

?? ?上面的代碼看上去沒有問題了,但是大家看看代碼的匯編代碼,看看是不是存在隱患。因為還會出現兩個人同時喂食的情況,

[cpp] view plaincopy
  1. 23:???????while(1){??
  2. 004010E8???mov?????????eax,1??
  3. 004010ED???test????????eax,eax??
  4. 004010EF???je??????????do_action+56h?(00401126)??
  5. 24:???????????if(?flag?==?true){??
  6. 004010F1???cmp?????????dword?ptr?[flag?(00433e04)],1??
  7. 004010F8???jne?????????do_action+43h?(00401113)??
  8. 25:???????????????flag?=?false;??
  9. 004010FA???mov?????????dword?ptr?[flag?(00433e04)],0??
  10. 26:???????????????do_give_fish_food();??
  11. 00401104???call????????@ILT+15(do_give_fish_food)?(00401014)??
  12. 27:???????????????flag?=?true;??
  13. 00401109???mov?????????dword?ptr?[flag?(00433e04)],1??
  14. 28:???????????}??
  15. 29:??
  16. 30:???????????Sleep(0);??
  17. 00401113???mov?????????esi,esp??
  18. 00401115???push????????0??
  19. 00401117???call????????dword?ptr?[__imp__Sleep@4?(004361c4)]??
  20. 0040111D???cmp?????????esi,esp??
  21. 0040111F???call????????__chkesp?(004011e0)??
  22. 31:???????}??
  23. 00401124???jmp?????????do_action+18h?(004010e8)??
  24. 32:???}??

?? ?我們此時假設有兩個線程ab在不停地進行判斷和喂食操作。設置當前flag?=?true,此時線程a執行到004010F8處時,判斷魚還沒有喂食,正準備執行指令004010F8,但是還沒有來得及對falg進行設置,此時出現了線程調度。線程b運行到004010F8時,發現當前沒有人喂食,所以執行喂食操作。等到b線程喂食結束,運行到00401113的時候,此時又出現了調度。線程a有繼續運行,因為之前已經判斷了當前還沒有喂食,所以線程a繼續進行了喂食了操作。所以,可憐的魚,這一次就連續經歷了兩次喂食操作,估計有一部分魚要撐死了。

?? ?當然魚在這里之所以會出現撐死的情況,主要是因為line?24line?25之間出現了系統調度。所以,我們在編寫程序的時候必須有一個牢固的思想意識,如果缺少必須要的手段,程序可以任何時刻任何地點被調度,那此時公共數據的計算就會出現錯誤。

?? ?那么有沒有方法避免這種情況的發生呢?當然有。朋友們可以繼續關注下面的博客。


3.? 多線程的那點事兒(之數據互斥)

? 在多線程存在的環境中,除了堆棧中的臨時數據之外,所有的數據都是共享的。如果我們需要線程之間正確地運行,那么務必需要保證公共數據的執行和計算是正確的。簡單一點說,就是保證數據在執行的時候必須是互斥的。否則,如果兩個或者多個線程在同一時刻對數據進行了操作,那么后果是不可想象的。

??? 也許有的朋友會說,不光數據需要保護,代碼也需要保護。提出這個觀點的朋友只看到了數據訪問互斥的表象。在程序的運行空間里面,什么最重要的呢?代碼嗎?當然不是。代碼只是為了數據的訪問存在的。數據才是我們一切工作的出發點和落腳點。

??? 那么,有什么辦法可以保證在某一時刻只有一個線程對數據進行操作呢?四個基本方法:

??? (1)關中斷

??? (2)數學互斥方法

??? (3)操作系統提供的互斥方法

??? (4)cpu原子操作

??? 為了讓大家可以對這四種方法有詳細的認識,我們可以進行詳細的介紹。

??

??? (1)關中斷

??? 要讓數據在某一時刻只被一個線程訪問,方法之一就是停止線程調度就可以了。那么怎樣停止線程調度呢?那么關掉時鐘中斷就可以了啊。在X86里面的確存在這樣的兩個指令,

[cpp] view plaincopy
  1. #include?<stdio.h>??
  2. ??
  3. int?main()??
  4. {??
  5. ????__asm{??
  6. ????????cli??
  7. ????????sti??
  8. ????}??
  9. ????return?1;??
  10. }??

??? 其中cli是關中斷,sti是開中斷。這段代碼沒有什么問題,可以編過,當然也可以生成執行文件。但是在執行的時候會出現一個異常告警:Unhandled exception in test.exe: 0xC0000096:? Privileged Instruction。告警已經說的很清楚了,這是一個特權指令。只有系統或者內核本身才可以使用這個指令。

??? 不過,大家也可以想象一下。因為平常我們編寫的程序都是應用級別的程序,要是每個程序都是用這些代碼,那不亂了套了。比如說,你不小心安裝一個低質量的軟件,說不定什么時候把你的中斷關了,這樣你的網絡就斷了,你的輸入就沒有回應了,你的音樂什么都沒有了,這樣的環境你受的了嗎?應用層的軟件是千差萬別的,軟件的水平也是參差不齊的,所以系統不可能相信任何一個私有軟件,它相信的只是它自己。

?

??? (2)數學方法

??? 假設有兩個線程(a、b)正要對一個共享數據進行訪問,那么怎么做到他們之間的互斥的呢?其實我們可以這么做,

[cpp] view plaincopy
  1. unsigned?int?flag[2]?=?{0};??
  2. unsigned?int?turn?=?0;??
  3. ??
  4. void?process(unsigned?int?index)??
  5. {??
  6. ????flag[index]?=?1;??
  7. ????turn?=??index;??
  8. ??
  9. ????while(flag[1?-?index]?&&?(turn?==??index));??
  10. ????do_something();??
  11. ????flag[index]?=?0;??
  12. }??

??? 其實,學過操作系統的朋友都知道,上面的算法其實就是Peterson算法,可惜它只能用于兩個線程的數據互斥。當然,這個算法還可以推廣到更多線程之間的互斥,那就是bakery算法。但是數學算法有兩個缺點:

??? a)占有空間多,兩個線程就要flag占兩個單位空間,那么n個線程就要n個flag空間,

??? b)代碼編寫復雜,考慮的情況比較復雜

?

??? (3)系統提供的互斥算法

??? 系統提供的互斥算法其實是我們平時開發中用的最多的互斥工具。就拿windows來說,關于互斥的工具就有臨界區、互斥量、信號量等等。這類算法有一個特點,那就是都是依據系統提高的互斥資源,那么系統又是怎么完成這些功能的呢?其實也不難。

??? 系統加鎖過程,

[cpp] view plaincopy
  1. void?Lock(HANDLE?hLock)??
  2. {??
  3. ????__asm?{cli};??
  4. ??
  5. ????while(1){??
  6. ????????if(/*?鎖可用*/){??
  7. ????????????/*?設定標志,表明當前鎖已被占用?*/??
  8. ????????????__asm?{sti};??
  9. ????????????return;??
  10. ????????}??
  11. ??
  12. ????????__asm{sti};??
  13. ????????schedule();??
  14. ????????__asm{cli};??
  15. ????}??
  16. }??

??? 系統解鎖過程,

[cpp] view plaincopy
  1. void?UnLock(HANDLE?hLock)??
  2. {??
  3. ????__asm?{cli};??
  4. ????/*?設定標志,?當前鎖可用?*/??
  5. ????__asm{sti};??
  6. }??

??? 上面其實討論的就是一種最簡單的系統鎖情況。中間沒有涉及到就緒線程的壓入和彈出過程,沒有涉及到資源個數的問題,所以不是很復雜。朋友們仔細看看,應該都可以明白代碼表達的是什么意思。

?

??? (4)CPU的原子操作
??? 因為在多線程操作當中,有很大一部分是比較、自增、自減等簡單操作。因為需要互斥的代碼很少,所以使用互斥量、信號量并不合算。因此,CPU廠商為了開發的方便,把一些常用的指令設計成了原子指令,在windows上面也被稱為原子鎖,常用的原子操作函數有

[cpp] view plaincopy
  1. InterLockedAdd??
  2. ??
  3. InterLockedExchange??
  4. ??
  5. InterLockedCompareExchange??
  6. ??
  7. InterLockedIncrement??
  8. ??
  9. InterLockedDecrement??
  10. ??
  11. InterLockedAnd??
  12. ??
  13. 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
  1. static?inline?void?__raw_spin_lock(raw_spinlock_t?*lock)??
  2. {??
  3. ????asm?volatile("\n1:\t"??
  4. ?????????????LOCK_PREFIX?"?;?decb?%0\n\t"??
  5. ?????????????"jns?3f\n"??
  6. ?????????????"2:\t"??
  7. ?????????????"rep;nop\n\t"??
  8. ?????????????"cmpb?$0,%0\n\t"??
  9. ?????????????"jle?2b\n\t"??
  10. ?????????????"jmp?1b\n"??
  11. ?????????????"3:\n\t"??
  12. ?????????????:?"+m"?(lock->slock)?:?:?"memory");??
  13. }??
上面這段代碼是怎么做到自旋鎖的呢?我們可以一句一句看看,


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
  1. InitializeCriticalSection??
  2. EnterCriticalSection??
  3. LeaveCriticalSection??
  4. DeleteCriticalSection??
?? ?如果想要對數據進行互斥操作的話,也很簡單,這樣做就可以了,
[cpp] view plaincopy
  1. EnterCriticalSection(/*...*/)??
  2. ????do_something();??
  3. LeaveCriticalSection(/*...*/)??

?? ? (2)互斥鎖
?? ?互斥鎖也是一種鎖。和臨界區不同的是,它可以被不同進程使用,因為它有名字。同時,獲取鎖和釋放鎖的線程必須是同一個線程。常用的互斥鎖操作有
[cpp] view plaincopy
  1. CreateMutex??
  2. OpenMutex??
  3. ReleaseMutex??
?? ?那么,怎么用互斥鎖進行數據的訪問呢,其實不難。
[cpp] view plaincopy
  1. WaitForSingleObject(/*...*/);??
  2. ????do_something();??
  3. ReleaseMutex(/*...*/);??

?? ? (3)信號量
?? ?信號量是使用的最多的一種鎖結果,也是最方便的一種鎖。圍繞著信號量,人們提出了很多數據互斥訪問的方案,pv操作就是其中的一種。如果說互斥鎖只能對單個資源進行保護,那么信號量可以對多個資源進行保護。同時信號量在解鎖的時候,可以被另外一個thread進行解鎖操作。目前,常用的信號量操作有,
[cpp] view plaincopy
  1. CreateSemaphore??
  2. OpenSemaphore??
  3. ReleaseSemaphore??
?? ?信號量的使用和互斥鎖差不多。關鍵是信號量在初始化的時候需要明確當前資源的數量和信號量的初始狀態是什么,
[cpp] view plaincopy
  1. WaitForSingleObject(/*...*/);??
  2. ????do_something();??
  3. ReleaseSemaphore(/*...*/);??

?? ? (4)event對象
?? ?event對象是windows下面很有趣的一種鎖結果。從某種意義上說,它和互斥鎖很相近,但是又不一樣。因為在thread獲得鎖的使用權之前,常常需要main線程調用SetEvent設置一把才可以。關鍵是,在thread結束之前,我們也不清楚當前thread獲得event之后執行到哪了。所以使用起來,要特別小心。常用的event操作有,
[cpp] view plaincopy
  1. CreateEvent??
  2. OpenEvent??
  3. PulseEvent??
  4. ResetEvent??
  5. SetEvent??
?? ?我們對event的使用習慣于分成main thread和normal thread使用。main thread負責event的設置和操作,而normal thread負責event的等待操作。在CreateEvent的時候,要務必考慮清楚event的初始狀態和基本屬性。
?? ?對于main thread,應該這么做,
[cpp] view plaincopy
  1. CreateEvent(/*...*/);??
  2. SetEvent(/*...*/);??
  3. WaitForMultiObjects(hThread,?/*...*/);??
  4. CloseHandle(/*...*/);??

?? ?對于normal thread來說,操作比較簡單,

[cpp] view plaincopy
  1. while(1){??
  2. ????WaitForSingleObject(/*...*/);??
  3. ??
  4. ????/*...*/??
  5. }??

總結:
?? ?(1)關于 臨界區、 互斥區、 信號量、 event在msdn上均有示例代碼
?? ?(2)一般來說,使用頻率上信號量 > 互斥區 > 臨界區 > 事件對象
?? ?(3)信號量可以實現其他三種鎖的功能,學習上應有所側重
?? ?(4)紙上得來終覺淺,多實踐才能掌握它們之間的區別



6.

多線程的那點兒事(之C++鎖)

? 編寫程序不容易,編寫多線程的程序更不容易。相信編寫過多線程的程序都應該有這樣的一個痛苦過程,什么樣的情況呢?朋友們應該看一下代碼就明白了,
[cpp] view plaincopy
  1. void?data_process()??
  2. {??
  3. ????EnterCriticalSection();??
  4. ????
  5. ????if(/*?error?happens?*/)??
  6. ????{??
  7. ????????LeaveCriticalSection();??
  8. ????????return;??
  9. ????}??
  10. ??
  11. ????if(/*?other?error?happens?*/)??
  12. ????{??
  13. ????????return;??
  14. ????}??
  15. ??
  16. ????LeaveCriticalSection();??
  17. }??
?? ?上面的代碼說明了一種情形。這種多線程的互斥情況在代碼編寫過程中是經常遇到的。所以,每次對共享數據進行操作時,都需要對數據進行EnterCriticalSection和LeaveCriticalSection的操作。但是,這中間也不是一帆風順的。很有可能你會遇到各種各樣的錯誤。那么,這時候你的程序就需要跳出去了。可能一開始遇到error的時候,你還記得需要退出臨界區。但是,如果錯誤多了,你未必記得還有這個操作了。這一錯就完了,別的線程就沒有機會獲取這個鎖了。
?? ?那么,有沒有可能利用C++的特性,自動處理這種情況呢?還真有。我們看看下面這個代碼,
[cpp] view plaincopy
  1. class?CLock??
  2. {??
  3. ????CRITICAL_SECTION&?cs;??
  4. ??
  5. public:??
  6. ????CLock(CRITICAL_SECTION&?lock):cs(lock){??
  7. ????????EnterCriticalSection(&cs);??
  8. ????}??
  9. ??
  10. ????~CLock()?{??
  11. ????????LeaveCriticalSection(&cs);??
  12. ????}??
  13. }??
  14. ??
  15. class?Process??
  16. {??
  17. ????CRITICAL_SECTION?cs;??
  18. ????/*?other?data?*/??
  19. ??
  20. public:??
  21. ????Process(){??
  22. ????????InitializeCriticalSection(&cs);??
  23. ????}??
  24. ??
  25. ????~Process()?{DeleteCriticalSection(&cs);}??
  26. ??
  27. ????void?data_process(){??
  28. ????????CLock?lock(cs);??
  29. ??
  30. ????????if(/*?error?happens?*/){??
  31. ????????????return;??
  32. ????????}??
  33. ??
  34. ????????return;??
  35. ????}??
  36. }??
?? ?C++的一個重要特點就是,不管函數什么時候退出,系統都會自動調用類的析構函數。在Process類的data_process函數中,,函數在開始就創建了一個CLock類。那么,在創建這個類的時候,其實就開始了臨界區的pk。那么一旦進入到臨界區當中,在error中能不能及時退出臨界區呢?此時,c++析構函數的優勢出現了。因為不管錯誤什么時候出現,在函數退出之前,系統都會幫我們善后。什么善后呢?就是系統會調用CLock的析構函數,也就是退出臨界區。這樣,我們的目的就達到了。
?? ?其實,這就是一個c++的trick。

7.

多線程的那點兒事(之原子鎖)

?原子鎖是多線程編程中的一個特色。然而,在平時的軟件編寫中,原子鎖的使用并不是很多。這其中原因很多,我想主要有兩個方面。第一,關于原子鎖這方面的內容介紹的比較少;第二,人們在編程上面習慣于已有的方案,如果沒有特別的需求,不過貿然修改已存在的代碼。畢竟對很多人來說,不求有功,但求無過。保持當前代碼的穩定性還是很重要的。 ?
?? ?其實,早在《 多線程數據互斥》這篇博客中,我們就已經介紹過原子鎖。本篇博客主要討論的就是原子鎖怎么使用。中間的一些用法只是我個人的一些經驗,希望能夠拋磚引玉,多聽聽大家的想法。

?? ?(1)查找函數中原子鎖?? ?

?? ?在一些函數當中,有的時候我們需要對滿足某種特性的數據進行查找。在傳統的單核CPU上,優化的空間比較有限。但是,現在多核CPU已經成了主流配置。所以我們完全可以把這些查找工作分成幾個子函數分在幾個核上面并行運算。但是,這中間就會涉及到一個問題,那就是對公共數據的訪問。傳統的訪問方式,應該是這樣的,

[cpp] view plaincopy
  1. unsigned?int?count?=?0;??
  2. ??
  3. int?find_data_process()??
  4. {??
  5. ????if(/*?data?meets?our?standards?*/){??
  6. ?????????EnterCriticalSection(&cs);??
  7. ?????????count?++;??
  8. ?????????LeaveCriticalSection(&cs);???????????
  9. ????}??
  10. }??

?? ?我們看到代碼中間使用到了鎖,那么勢必會涉及到系統調用和函數調度。所以,在執行效率上會大打折扣。那么如果使用原子鎖呢?
[cpp] view plaincopy
  1. unsigned?int?count?=?0;??
  2. ??
  3. int?find_data_process()??
  4. {??
  5. ????if(/*?data?meets?our?standards?*/){??
  6. ????????InterLockedIncrement(&count);??
  7. ????}??
  8. }??

?? ?有興趣的朋友可以做這樣一道題目,查看0~0xFFFFFFFF上有多少數可以被3整除?大家也可以驗證一下用原子鎖代替臨界區之后,代碼的效率究竟可以提高多少。關于多核多線程的編程,朋友們可以參考《多線程基礎篇》這篇博客。


?? ?(2)代碼段中的原子鎖
?? ?上面的范例只是介紹了統計功能中的原子鎖。那么怎么用原子鎖代替傳統的系統鎖呢?比如說,假設原來的數據訪問是這樣的,

[cpp] view plaincopy
  1. void?data_process()??
  2. {??
  3. ????EnterCriticalSection(&cs);??
  4. ????do_something();??
  5. ????LeaveCriticalSection(&cs);?????
  6. }??
?? ?如果改成原子鎖呢,會是什么樣的呢?
[cpp] view plaincopy
  1. unsigned?int?lock?=?0;??
  2. ??
  3. void?data_process()??
  4. {??
  5. ????while(1?==?InterLockedCompareExchange(&lock,?1,?0));??
  6. ????do_something();??
  7. ????lock?=?0;??????
  8. }??

?? ?這里用原子鎖代替普通的系統鎖,完成的功能其實是一樣的。那么這中間有什么區別呢?其實,關鍵要看do_something要執行多久。打個比方來說,現在我們去買包子,但是買包子的人很多。那怎么辦呢?有兩個選擇,如果賣包子的人手腳麻利,服務一個顧客只要10秒鐘,那么即使前面排隊的有50個人,我們只要等7、8分鐘就可以,這點等的時間還是值得的;但是如果不幸這個賣包子的老板服務一個顧客要1分鐘,那就悲催了,假使前面有50個人,那我們就要等50多分鐘了。50分鐘對我們來說可是不短的一個時間,我們完全可以利用這個時間去買點水果,交交水電費什么的,過了這個時間點再來買包子也不遲。


?? ?和上面的例子一樣,忙等的方法就是原子鎖,過一會再來的方法就是哪個傳統的系統鎖。用哪個,就看這個do_something的時間值不值得我們等待了。


8.

多線程的那點兒事(之讀寫鎖)

在編寫多線程的時候,有一種情況是十分常見的。那就是,有些公共數據修改的機會比較少。相比較改寫,它們讀的機會反而高的多。通常而言,在讀的過程中,往往伴隨著查找的操作,中間耗時很長。給這種代碼段加鎖,會極大地降低我們程序的效率。那么有沒有一種方法,可以專門處理這種多讀少寫的情況呢?
?? ?有,那就是讀寫鎖。

?? ?(1)首先,我們定義一下基本的數據結構。

[cpp] view plaincopy
  1. typedef?struct?_RWLock??
  2. {??
  3. ????int?count;??
  4. ????int?state;??
  5. ????HANDLE?hRead;??
  6. ????HANDLE?hWrite;??
  7. }RWLock;?????
?? ?同時,為了判斷當前的鎖是處于讀狀態,還是寫狀態,我們要定義一個枚舉量,
[cpp] view plaincopy
  1. typedef?enum??
  2. {??
  3. ????STATE_EMPTY?=?0,??
  4. ????STATE_READ,??
  5. ????STATE_WRITE??
  6. };??
?? ? (2)初始化數據結構
[cpp] view plaincopy
  1. RWLock*?create_read_write_lock(HANDLE?hRead,?HANDLE?hWrite)??
  2. {??
  3. ????RWLock*?pRwLock?=?NULL;??
  4. ??
  5. ????assert(NULL?!=?hRead?&&?NULL?!=?hWrite);??
  6. ????pRwLock?=?(RWLock*)malloc(sizeof(RWLock));??
  7. ????
  8. ????pRwLock->hRead?=?hRead;??
  9. ????pRwLock->hWrite?=?hWrite;??
  10. ????pRwLock->count?=?0;??
  11. ????pRwLock->state?=?STATE_EMPTY;??
  12. ????return?pRwLock;??
  13. }??
?? ? (3)獲取讀鎖
[cpp] view plaincopy
  1. void?read_lock(RWLock*?pRwLock)??
  2. {??
  3. ????assert(NULL?!=?pRwLock);??
  4. ??????
  5. ????WaitForSingleObject(pRwLock->hRead,?INFINITE);??
  6. ????pRwLock->counnt?++;??
  7. ????if(1?==?pRwLock->count){??
  8. ????????WaitForSingleObject(pRwLock->hWrite,?INFINITE);??
  9. ????????pRwLock->state?=?STATE_READ;??
  10. ????}??
  11. ????ReleaseMutex(pRwLock->hRead);??
  12. }??
?? ? (4)獲取寫鎖
[cpp] view plaincopy
  1. void?write_lock(RWLock*?pRwLock)??
  2. {??
  3. ????assert(NULL?!=?pRwLock);??
  4. ??
  5. ????WaitForSingleObject(pRwLock->hWrite,?INFINITE);??
  6. ????pRwLock->state?=?STATE_WRITE;??
  7. }??
?? ? (5)釋放讀寫鎖
[cpp] view plaincopy
  1. void?read_write_unlock(RWLock*?pRwLock)??
  2. {??
  3. ????assert(NULL?!=?pRwLock);??
  4. ??
  5. ????if(STATE_READ?==?pRwLock->state){??
  6. ????????WaitForSingleObject(pRwLock->hRead,?INFINITE);??
  7. ????????pRwLock->count?--;??
  8. ????????if(0?==?pRwLock->count){??
  9. ????????????pRwLock->state?=?STATE_EMPTY;??
  10. ????????????ReleaseMutex(pRwLock->hWrite);??
  11. ????????}??
  12. ????????ReleaseMutex(pRwLock->hRead);??
  13. ????}else{??
  14. ????????pRwLock->state?=?STATE_EMPTY;??
  15. ????????ReleaseMutex(pRwLock->hWrite);??
  16. ????}??
  17. ??????
  18. ????return;??
  19. }??

文章總結:
?? ?(1)讀寫鎖的優勢只有在多讀少寫、代碼段運行時間長這兩個條件下才會效率達到最大化;
?? ?(2)任何公共數據的修改都必須在鎖里面完成;
?? ?(3)讀寫鎖有自己的應用場所,選擇合適的應用環境十分重要;
?? ?(4)編寫讀寫鎖很容易出錯,朋友們應該多加練習;
?? ?(5)讀鎖和寫鎖一定要分開使用,否則達不到效果。




本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/450024.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/450024.shtml
英文地址,請注明出處:http://en.pswp.cn/news/450024.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Android應用開發—AsyncTask

摘錄自 Android 多線程—–AsyncTask詳解 AsyncTask AsyncTask&#xff1a;異步任務&#xff0c;從字面上來說&#xff0c;就是在我們的UI主線程運行的時候&#xff0c;異步的完成一些操作。AsyncTask允許我們的執行一個異步的任務在后臺。我們可以將耗時的操作放在異步任務當…

std::shared_ptr之deleter的巧妙應用

本文由作者鄒啟文授權網易云社區發布。std::shared_ptr一次創建&#xff0c;多處共享&#xff0c;通過引用計數控制生命周期。 實例 在郵箱大師PC版中&#xff0c;我們在實現搜索時&#xff0c;大致思路是這樣的&#xff1a; 每一個賬號都有一個SearchFlow&#xff0c;搜索開始…

js - 執行上下文和作用域以及閉包

首先&#xff0c;咱們通常被"執行上下文"&#xff0c;"執行上下文環境"&#xff0c;"上下文環境"&#xff0c;"執行上下文棧"這些名詞搞混。那我們一一來揭秘這些名字的含義。 這一塊一直比較晦澀難懂&#xff0c;還是需要仔細去斟酌斟…

Spring之JDBCTemplate

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一、Spring對不同的持久化支持&#xff1a; Spring為各種支持的持久化技術&#xff0c;都提供了簡單操作的模板和回調 ORM持久化技術模…

從螞蟻金服實踐入手,帶你深入了解 Service Mesh

本文整理自螞蟻金服高級技術專家敖小劍在 QCon 上海 2018 上的演講。我是來自螞蟻金服中間件團隊的敖小劍&#xff0c;目前是螞蟻金服 Service Mesh 項目的 PD。我同時也是 Servicemesher 中國技術社區的創始人&#xff0c;是 Service Mesh 技術在國內最早的布道師。我今天給大…

Android應用開發—FragmentManager如何管理fragments

本文主要摘錄自Android中使用FragmentManager管理fragments 和 淺談FragmentManager與fragment之一二事 先講下自己對fragment的理解&#xff1a; 對于fragment&#xff0c;有太多官方文檔和博文來介紹&#xff0c;此處不做轉述&#xff1a;我感覺android提供fragment這種組件…

數組指針 和 指針數組

最近發現公司有些人說怎樣區分 數組指針 和 指針數組 &#xff1f; 其實 很簡單&#xff1b; 數組指針&#xff0c; 先是&#xff08;定語 &#xff09; &#xff08;主體&#xff09;&#xff0c; &#xff08;定語 數組&#xff09; &#xff08;主體 指針&#xff09…

在云服務器上注意GeoServer和ShadowDataMap的跨域設置

在云服務器上注意GeoServer和ShadowDataMap的跨域設置 1、對于支持cors的網絡資源 可以在ShadowDataMap的devserverconfig.json里設置相應的跨域資源 提示&#xff1a;geoserver發布的地圖服務雖然同在一個服務器上&#xff0c;但是端口不一樣&#xff0c;同樣需要設置跨域 如&…

Guava ImmutableCollection簡介

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 ImmutableCollection代碼定義 GwtCompatible(emulatedtrue) public abstract class ImmutableCollection<E> extends AbstractCo…

Todo List

fragment里面如何處理back按鍵事件。 fragment里面無法Override onBackPressed接口&#xff0c;如何優雅的處理back press事件&#xff1f;activity如何獲取當前活躍的fragment對象。異步網絡請求如何改造成rxjava&#xff0c;rxjava有設置運行線程的能力&#xff0c;異步請求…

常見的幾種負載均衡算法

1、輪詢將所有請求&#xff0c;依次分發到每臺服務器上&#xff0c;適合服務器硬件相同的場景。優點&#xff1a;服務器請求數目相同&#xff1b; 缺點&#xff1a;服務器壓力不一樣&#xff0c;不適合服務器配置不同的情況&#xff1b; 2、隨機請求隨機分配到各臺服務器上。優…

基于 Token 的身份驗證方法

基于 Token 的身份驗證方法 使用基于 Token 的身份驗證方法&#xff0c;在服務端不需要存儲用戶的登錄記錄。大概的流程是這樣的&#xff1a;客戶端使用用戶名跟密碼請求登錄 服務端收到請求&#xff0c;去驗證用戶名與密碼 驗證成功后&#xff0c;服務端會簽發一個 Token&…

Android應用開發-圖片加載庫Glide

Glide Picasso和Glide之間的區別&#xff1a; Picasso 僅僅緩存了全尺寸的圖像&#xff1b;然而 Glide 緩存了原始圖像&#xff0c;全分辨率圖像和另外小版本的圖像。

excel 表格導入 - java 實現

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 import com.alibaba.druid.support.json.JSONUtils; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; imp…

C語言 API

MySQL的C語言API接口 1、首先當然是連接數據庫&#xff0c;函數原型如下&#xff1a; MYSQL * STDCALL mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned…

線程組之間的JMeter傳遞變量

下面&#xff0c;我們將看看如何在線程組之間共享和傳遞變量。在開發高級JMeter腳本時&#xff0c;很可能您將擁有多個線程組。每個線程組將執行不同的請求。一個很好的例子是我們需要使用Bearer Tokens對用戶進行身份驗證。一個線程組執行身份驗證并保存令牌。另一個線程組需要…

python第九天(9-33)

一&#xff1a;進程 進程概念 進程就是一個程序運行在一個數據集上的一次動態執行過程進程一般由程序&#xff0c;數據集&#xff0c;進程控制塊組成進程控制塊&#xff1a; 進程控制塊用來記錄進程的外部特征&#xff0c;描述進程的執行變化過程&#xff0c;系統可以利用它來控…

Android Studio出現Failed to open zip file. Gradle's dependency cache may be corrupt問題的解決

刪除了/Users/tycao/.gradle/wrapper/dists目錄下對應的gradle-X.X-all目錄重新sync了

雙機熱備份和負載均衡的區別

1、雙機熱備相當于2臺服務器其中有一臺是另一臺的備機&#xff0c;也可以互為備機&#xff1b;而且這兩臺主機的數據時實時同步的&#xff1b;主機在運行服務時&#xff0c;備機處于檢測狀態&#xff0c;主機發生故障后&#xff0c;備機將接管主機的服務。2、負載均衡是在這2臺…

Python 數據類型--Bytes類型

一、Bytes類型 在Python3以后&#xff0c;字符串和bytes類型徹底分開了。字符串是以字符為單位進行處理的&#xff0c;bytes類型是以字節為單位處理的。 bytes數據類型在所有的操作和使用甚至內置方法上和字符串數據類型基本一樣&#xff0c;也是不可變的序列對象。 bytes對象只…