在計算機執行過程當中,對於多個任務,它們共享着一個資源,要求對該資源的存取過程是排他的。c++
不考慮SMP狀況,僅分析單CPU狀況,由於SMP只不過是更復雜的一種狀況,原理相似。
有以下代碼片斷,其中share_data是一個全局變量。編程
int share_data = 0; void foo(void) { share_data++; //寫共享資源 show share_data; //讀共享資源 }
若是兩個線程都執行上述片斷,執行過程以下圖所示:
首先是Thread A開始執行,此時share_data爲0,執行完①後,若是執行流程不被打斷,則繼續往下走,最終show share_data獲得的數據應該是1。可是當Thread A執行完步驟①後,CPU發生了調度,切到Thread B上執行②,而share_data對於兩個線程是共享的,因此Thread B此刻看到的share_data是通過了一次自增的,爲1。最終執行完流程③,④。Thread A看到的結果是share_data自增了兩次,與預期不符。併發
還一種狀況是代碼片斷分別在一個線程和中斷中:
性能
過程和上面相似,只不過執行流程是被中斷打斷的。測試
以上多個執行流在一個時間段併發(Concurrency)執行,而且圍繞共享資源進行訪問致使了競態(Race Conditions)。就是一種你爭我奪的狀態。而這個共享的資源就是他們爭奪的對象,能夠是(全局的變量,同一個硬件資源,文件系統上的一個文件等)。優化
與併發經常一塊兒提到的另外一個詞叫並行,並行指同一個時刻,同時進行,例如SMP上多個CPU執行多個線程這種狀況。ui
以上看到了競態帶來了咱們預期以外的效果,按照咱們程序的設計,對於一個功能模塊,一樣的輸入應該獲得一致的輸出才行。考慮生活中一個這樣的問題:A和B住一塊兒,冰箱裏面沒麪包了就要去買,可是咱們要避免兩我的重複買麪包這種狀況。如何解決?操作系統
do { if (noNote) { if (noBread) { leave Note; buy bread; remove Note; } } }while (1);
能解決問題嗎?不能。假設A看到 if (noNote) ---> if (noBread)
,恰好有別的事打斷了他,A出去了,這時B進來,也看到了if (noNote) ---> if (noBread)
,而後恰巧B也被打斷了,A此時進來,因而他繼續前面被打斷的工做,留下紙條,買回麪包,撕去紙條,走了。而後B回來了,繼續前面被打斷的工做,留下紙條,買回麪包,撕去紙條,走了。這時會發現A和B都買了麪包。線程
do{ leave Note; if (noNote) { if (noBread) { buy bread; remove Note; } } }while(1);
分析這個過程看獲得,雙方留下紙條後,進去檢查if (noNote)
都是不成立的,這樣,兩我的都不會去買麪包,若是這種巧合一直按照這種順序發生,那麼永遠不會有人買麪包。設計
do{ leave my Note; if (no Other's Note) { if (noBread) { buy Bread; } remove my Note; } }while(1);
顯而易見,依舊是不行的。
do{ leave Note1; while (is Note2 Exist) { do nothing; } if (noBread) { buy Bread; } remove Note1; }while(1);
do{ leave Note2; if (noNote1) { if(noBread) { buy Bread; } } remove Note2; }while(1);
這個方案可行嗎?可行。可是明顯看的出和前面3個方案的不一樣,這裏用了兩段不一樣的代碼。此方案有以下缺點:
- 一樣是2我的買麪包,上面三種方案,購買流程一套代碼便可實現,而此狀況須要2套代碼。
- 若是處理線程數超過2個,處理邏輯的複雜度會呈指數級增加。
- 而且第一段處理有一個死循環,若是下面一段的處理比較耗時,則上一段的死循環會持續好久,致使比較高的CPU佔用。
do { while(check_and_leave()); //執行不可打斷 if (noBread) { buy Bread; } remove Note; }while(1);
check_and_leave()裏面的邏輯是:
if (noNote) { leave Note; return FALSE; } else { return TRUE; }
這樣就能夠解決競態問題了。只要實現check_and_leave()執行動做的不可打斷就能夠了。
enter section; critical section; exit section; remainder section;
禁止中斷至關因而硬件方法實現,在第三節的分析可知,臨界區出現了競態主要是由於任務調度引發,或者中斷引發。而任務調度也是由中斷方法實現(如時間片,超時則引起調度,由時鐘中斷致使),因此禁止了中斷,能夠實現臨界區的訪問。
local_irq_save(unsigned int flags); critical section; local_irq_restore(unsigned int flags);
由於這些侷限性,要當心使用禁用條件中斷。
基於硬件原子性操做的高級抽象方法。硬件提供了一些原語,像中斷禁用、原子操做指令等,操做系統在此基礎上提供更高級的編程抽象來簡化並行編程,例如鎖、信號量,這些方式是從硬件原語中構建的。
那麼如何實現這種抽象呢?
Test And Set
bool Test_And_Set(bool* flag) { bool rv = *flag; *flag = TRUE; return rv; }
這是一條機器指令,這條機器指令完成了一般操做的讀寫兩條機器指令的工做,完成了三件事情:
Exchange
void Exchange(bool *a, bool *b) { bool tmp = *a; *a = *b; *b = tmp; }
雖然這兩個指令看起來由幾條小指令組成,可是它已經被封裝成了一條機器指令,這就意味着它在執行的時候不會被打斷,不容許出現中斷或切換,這就是機器指令的語義保證。在此基礎上完成互斥。
使用Test_And_Set實現自旋鎖:
這裏看到若是鎖狀態value
爲1,表示臨界區如今被人佔用,已經上鎖了,這時調用Lock::Acquire()
不能獲得鎖,那麼它會在這裏死循環,不斷測試value的值。這種空轉全用來消耗任務的時間片,顯然是能夠有優化空間的。例如發現獲取鎖失敗,則將任務置爲睡眠狀態,掛入到等待隊列。
忙等和非忙等各有各的使用場景,若是臨界區比較簡單,執行不會消耗多少時間,那麼用忙等更合適,由於非忙等是須要進行任務調度的,調度出去,而後再調度進來,這個調度的開銷不能白白浪費在適合忙等的臨界區。
使用exchange實現鎖:
class Lock{ int value = 0; } Lock::Acquire(){ int key = 1; while(1 == key){ exchange(lock, key); } } Lock::Reease(){ value = 0; }
不論是Test_And_Set
仍是Exchage
他們的終極目的其實都是在一個機器指令裏面對一個變量實現: