程序同步概念簡介 多執行緒上篇(四)
程序同步概念
臨界資源
一旦有對資源的共享,就必然涉及競爭限制
比如儘管有兩個人去水井打水,但是水井卻只有一個;合理安排的話剛好錯開,但是如果安排不合理,那就會出現衝突,出現衝突怎麼辦?總有一個先來後到,等下就好了。
這個水井就是一個臨界資源
臨界資源用來表示一種公共資源或者說是共享資料,可以被多個執行緒使用。
但是每一次,只能有一個執行緒使用它,一旦臨界資源被佔用,其他執行緒要想使用這個資源,就必須等待。
當多程序訪問臨界資源時,比如印表機
假設A程序和B程序輪流獲得CPU時間片執行,A列印數學,B列印英語,如果不進行任何的控制與限制,可能會打印出來一張紙上一道數學計算題,接下來是一段英語的情況。
所以儘管A程序和B程序輪流獲得時間片執行,但是當需要訪問臨界資源時,一旦有一個程序已經開始使用,另外的程序就不能進行使用,只能等待。
計算機就是那個計算機,硬碟就是那個硬碟,一段程式碼中的某個變數(共享變數)就是那個變數.....所有的一切都是隻有一份,如果對於某個點多程序同時訪問,必然要做一定的限制
程序同步的主要任務是對多個相關程序在執行次序上進行協調,以使併發執行的諸程序之間能有效地共享資源和相互合作,從而使程式的執行具有可再現性
兩種制約關係
既然資源訪問有限制,到底有哪些場景是需要同步處理的?也就是何時會出現資源衝突?
看得出來,其實同步要解決的問題根本就是競爭,間接關係是赤裸裸的的競爭,共享同一個I/O就是一種競爭,儘管他們看似好像沒有什麼關係
直接的制約關係,源於程序間的合作,某種程度上來說也是一種競爭,只不過是有條件的競爭,他們共享緩衝區,當緩衝區滿時只能是消費者可以執行,生產者需要阻塞,這可以認為緩衝區滿這種情況下,消費者獨佔了緩衝區,生產者不能使用了,不過這種情況下還是說成合作比較容易理解
所以,要麼是因為共享資源帶來的競爭,要麼就是相互合作帶來的依賴。
臨界區
有了臨界資源的概念,就很容易理解臨界區的概念,在程式中,所有的操作都是通過程式碼執行的,訪問臨界資源的那段程式碼就是臨界區
以打水為例,所以在還沒到井口,就要畫一個大圈,不允許第二個人進入範圍,“請站在安全黃線內”這句話熟悉麼?
這就是臨界區。
同步規則
如何才能夠合理處理競爭或者合作依賴導致的制約?
- 空閒讓進
- 忙則等待
- 有限等待
- 讓權等待
空閒讓進和忙則等待很好理解,對於臨界資源,如果空閒沒有被使用,誰來了之後都可以使用;如果臨界資源正在被使用,那麼其他後來者就需要進行等待
有限等待是指,要求訪問臨界資源的程序,應保證有限時間內能進入自己的臨界區,自己不能傻傻的等,傻傻等受傷的是自己
讓權等待是指,如果無法進入自己的臨界區時,應立即釋放處理機,而不能佔著CPU死等,你死等就算了,別人卻也不能用了
有限等待和讓權等待是兩個維度
你不能為了一件事情不顧一切代價等個天荒地老,太傷身了;
如果你非要花五塊錢去蘇寧買一臺電視(等待事件發生),人家不賣給你(無法進入臨界區),你就賴著不走(忙等),你就耽誤別人做生意了(別的程序無法獲得CPU)
有限等待和讓權等待的共同特性是必須保證有條件的退出以給其他程序提供執行的機會
簡單說就是有限時間內你就要走開,你得不到更要走開,你即使能得到但是時間太久也得先讓一下別人
臨界區的設定就是安全黃線的設定,同步規則其實就是臨界區兩條黃線進出規則
對於臨界區,還可以進一步細分出來進入區和退出區以及剩餘區
臨界區演算法
Peterson演算法
所以臨界區方式解決同步問題就是藉助於演算法,合理的控制對於臨界區的進入與退出,並且保障能夠做到:空閒讓進、忙則等待、有限等待、讓權等待
一種有名的演算法為 Peterson
Peterson演算法適用於兩個程序在臨界區與剩餘區間交替執行。假設兩個程序分別為p0 和 p1
為了表示方便,使用pi表示其中一個程序時,pj表示另外一個,顯然有i = 1-j(j = 1-i)
使用一個int型別變數turn 表示可以進入臨界區的執行緒,如果turn == i,表示pi可以進入臨界區
使用boolean 型別陣列flag,總共兩個程序,所以flag[2],用來表示哪個程序想要進入臨界區,如果flag[i] = true;表示程序pi想要進入臨界區
上圖紅框內為進入區,藍框內為退出區
為了進入臨界區,程序pi首先設定flag[i]為true;並且設定turn為j;
顯然,根據while的條件,只有flag[j] == false 或者turn == i 時,pi可以進入臨界區
也就是如果我想進入的話,當對方不想進入或者當前允許我進入時,我就可以進入臨界區
顯然,如果只有一個程序想要進入,那麼如上所述,對方不想進入時,可以進入臨界區,符合空閒讓進
如果兩個程序都想進入,不管經過多麼激烈的競爭,當執行到while時flag[0] 和 flag[1] 都是true,也就是while內部的兩個條件,條件1始終是true
但是turn只會有一個值,要麼0 要麼1,也就是說while的第2個條件決定了誰會被while阻塞,誰能夠繼續執行下去
這種情況下必然能夠有一個程序會進入臨界區,另外一個被while迴圈阻塞,所以符合空閒讓進、忙則等待
當臨界區內的程序執行結束後,會設定flag[] 標誌位,如果此時另外的程序在等待,一旦設定後,其他程序就可以進入臨界區(剛才已經說了,如果pi想進入,flag[j] == false 或者turn == i 時可以進入)也就是說當前程序結束後,下一個程序就能夠進入了,所以滿足有限等待
小結:
上面的演算法,滿足了通過進入區和退出區程式碼的設定,可以做到同步的規則
如果只有一個想要進入臨界區,可以直接進入,如果兩個競爭進入,只有一個能夠進入,另一個會被while迴圈阻塞
Peterson只是一種臨界區演算法,還有其他的
同步方式之訊號量
1965年,荷蘭學者Dijkstra 提出的訊號量(Semaphores)機制是一種卓有成效的程序同步工具。
臨界區演算法的原理可以讓多程序對於臨界區資源的訪問序列化;
訊號量機制允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目。
整型訊號量
最初訊號量機制被稱之為整型訊號量
最初由Dijkstra 把整型訊號量定義為一個用於表示資源數目的整型量 S,它與一般整型量不同,除初始化外,僅能通過兩個標準的原子操作(Atomic Operation) wait(S)和 signal(S)來訪問。
這兩個操作一直被分別稱為P、V操作(據說,據說因為Dijkstra是荷蘭人。在最初論文以及大多數文獻中,用P代表wait。用V代表signal。是荷蘭語中高度 proberen 和增量verhogen)
Wait(S)和 signal(S)操作可描述為:
wait(S):while (S<=0); S:=S-1; signal(S):S:=S+1;
wait表示資源申請:如果S小於等於0(資源不足)等待,如果滿足那麼將會進行S-1,也就是申請資源
signal表示資源釋放:每釋放一次資源,資源數目 S+1
P、V操作也稱之為操作原語,就是指原子操作
原子性 是指一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗(具體怎麼保證的,此處可以不用關注)
多個執行緒併發的對資源進行訪問時,藉助於PV原語操作,可以有效地做到共享資源的限制訪問。
但是,對於整型訊號量,P操作也就是 wait(S)
wait(S):while (S<=0); S:=S-1;
如果獲取不到資源,將會持續while (S<=0);,將會永遠等待,程序處於忙等狀態
關於原語
wait(S)和 signal(S)這一原子操作叫做原語,原語是作業系統概念的術語,是由若干條指令組成的,用於完成一定功能的一個過程
是由若干個機器指令構成的完成某種特定功能的一段程式,具有不可分割性,即原語的執行必須是連續的,在執行過程中不允許被中斷。
原語是作業系統的核心部分組成,原語有不可中斷性。它必須在管態(核心態,系統態)下執行,並且常駐記憶體,而個別系統有一部分不在管態下執行。
可以簡單的理解是具有指定功能的作業系統提供的一個API性質的一種東西
記錄型訊號量
鑑於整型訊號量機制中的“忙等”情況,演化出來記錄型訊號量
如果程序無法進入臨界區,那麼進入等待釋放CPU資源,並且通過一個連結串列記錄等待的程序。
記錄型訊號量機制在整形訊號量機制的基礎上增加了程序連結串列指標L,這也是記錄型訊號量名稱的由來
記錄型訊號量 semaphore 的結構如下:
semaphore { value:int value; L:程序等待連結串列(集合); }
value相當於整型訊號量中的S,L就是一個連結串列(集合)
簡言之,將整形訊號量中的整型S,演化為一個結構,這個結構包括一個整型值,還有一個等待的程序連結串列
相對應整型訊號量中的wait(S) 和 signal(S)可以描述為:
wait(S): var S = semaphore; S.value=S.value-1; if S.value<0 then block(S.L); signal(S): var S = semaphore; S.value=S.value+1; if S.value<=0 then wakeup(S.L);
上面的操作中,均定義了一個semaphore型別的變數S
- 如果執行 wait 操作,先執行資源減一,如果此時S.value<0,說明在申請資源之前(S.value-1),原來的資源就是<=0,那麼該程序阻塞,加入等待佇列L中
- 如果執行 signal 操作,先執行資源加一,如果此時S.value<=0,說明在釋放資源之前(),原來的資源是<0的,那麼將等待連結串列中的程序喚醒
上面邏輯的關鍵之處在於:
- 當申請資源時,先進行S.value-1,一旦資源出現負數,說明需要等待,S.value的絕對值就是等待程序的個數,也就是S.L的長度
- 當資源恢復時,先進行S.value+1,已經有人釋放資源瞭然而資源個數還是小於等於0,說明原來就有人在等待,所以應該去喚醒
block 和 wakeup 也都是原語,也就是原子操作。
block原語,進行自我阻塞,放棄處理機,並插入到訊號量連結串列S.L 中
wakeup原語,將S.L連結串列中的等待程序喚醒
如果 S.value的初值為 1,表示只允許一個程序訪問臨界資源,此時的訊號量轉化為互斥訊號量,用於程序互斥。(效果就如同Peterson演算法了)
AND 型訊號量
針對於臨界區演算法或者是整型訊號量或者是記錄型訊號量是針對各程序之間只共享一個臨界資源而言的。
但是有些場景下,一個程序需要先獲得兩個或更多的共享資源後方能執行其任務。
假設A,B兩個程序,均需要申請資源D,E
process A: process B:
wait(D); wait(E);
wait(E); wait(D);
假設交替執行順序如下
process A: wait(D); 於是D=0
process B: wait(E); 於是E=0
process A: wait(E); 於是E=-1 A阻塞
process B: wait(D); 於是D=-1 B阻塞
最終A,B都被阻塞,如果沒有外力作用下,兩者都無法從阻塞狀態釋放出來,這就是死鎖
相關概念
鎖
對於一個水井,你在打水,另外的人就要等一等,這是人類大腦意識做出來的很基本的反應(人眼識別,大腦解析並且做出反應)
但是計算機程式並沒有這麼智慧,你需要對他進行某些處理,以限制別的執行緒的訪問,比如你可以將“安全黃線”變成一個安全門,比如廁所,進去了之後把門關上。。。
這種概念就是鎖,鎖就是對資源施加控制,鎖指的是一種控制權
當進入臨界區時,我們稱之為獲得鎖,獲得鎖之後就可以訪問臨界資源;
其他執行緒想要進入臨界區,也需要先獲得鎖,顯然,他們獲取不到,因為此時,鎖被當前正在執行的執行緒持有
當前執行緒結束後,將會釋放鎖,別得執行緒就可以獲取這個資源的鎖,然後....
死鎖
鎖表示一種控制權,對臨界資源的訪問許可權,如果臨界資源不止一個,就可能出現這種情況:
需要先後訪問兩種臨界資源A和B,thread1獲得了A執行緒的鎖之後,等待獲得B的鎖,但是thread2獲得了資源B的鎖,在等待A資源的鎖,這就出現了互相等待的情況
比如一條窄橋,同一時刻僅僅允許一輛車通過,如果一旦出現兩輛車都走到橋的一半處,而且互不相讓,怎麼辦?這就是死鎖
解決方案
AND型訊號量機制就是用於解決這種多共享資源下的同步問題的
AND 同步機制的基本思想:
將程序在整個執行過程中需要的所有資源,一次性全部地分配給程序,待程序使用完後再一起釋放。
只要尚有一個資源未能分配給程序,其它所有可能為之分配的資源也不分配給它。
也就是對若干個臨界資源的分配,採取原子操作方式:要麼把它所請求的資源全部分配到程序,要麼一個也不分配。
這種思維就是通過對“若干個臨界資源“的原子分配,邏輯上就相當於一份共享資源,要麼得到,也麼得不到,所以就不會出現死鎖
在 wait 操作中,增加了一個“AND”條件,所以被稱為AND 同步,也被稱為同時wait操作,即Swait(Simultaneous wait),相應的signal被稱為Ssignal(Simultaneous signal)

Swait(S) 和 Ssignal(S)可以描述為:
Swait(S1,S2,…,Sn) if(Si>=1 and …and Sn>=1){ for( i=1 to n){ Si=Si-1; } }else{ 將程序插入到第一個資源<1 的對應的S的佇列中,並且程式計數器設定到Swait的開始(比如S1 S2 都大於等於1,但是 S3<1,那麼就插入到S3.L中;從頭再來就是為了整體分配) } Ssignal(S1,S2,…,Sn) for(i=1 to n){ Si=Si+1; 將與Si關聯的所有程序從等待佇列中刪除,移入到就緒佇列中 }
AND型訊號量機制藉助於對於多個資源的“批量處理”的原子操作方式,將多資源的同步問題,轉換為一個“同一批資源”的同步問題
訊號量集
記錄型訊號量機制中,只是對訊號量加1(S+1 或者S.value+1) 或者 減1(S-1 或者S.value-1)操作,也就是隻能獲得或者釋放一個單位的臨界資源
如果想要一次申請N個呢?
如果進行N次的資源申請怎麼辦,一種方式是可以使用多次Wait(S)操作,但是顯然效率較低;
另外有時候當資源數量低於某一下限值時,就不進行分配怎麼辦?需要在每次分配資源前檢查資源的數量。
為了解決這兩個問題,對AND訊號量機制進一步擴充套件,形成了一般化的“訊號量集”機制。
Swait操作可描述如下
其中S為訊號量,d為需求值,而 t為下限值。
Swait(S1,t1,d1,…,Sn,tn,dn) if( Si>=t1 and …and Sn>=tn){ for(i=1 to n){ Si=Si-di; } }else{ 將程序插入到第一個資源Si<ti 的對應的S的佇列中,並且程式計數器設定到Swait的開始(比如S1 S2 都大於等於t1,t2,但是 S3<t3,那麼就插入到S3.L中;從頭再來就是為了整體分配) } Ssignal(S1,d1,…,Sn,dn) for(i=1 to n){ Si=Si+di; 將與Si關聯的所有程序從等待佇列中刪除,移入到就緒佇列中 }
訊號量集就是AND型訊號量機制將資源限制擴充套件到Ti
- Swait(S,d,d)。此時在訊號量集中只有一個訊號量 S,但允許它每次申請 d 個資源,當現有資源數少於d時,不予分配。
- Swait(S,1,1)。此時的訊號量集已蛻化為一般的記錄型訊號量(S>1時)或互斥訊號量(S=1 時)。
- Swait(S,1,0)。這是一種很特殊且很有用的訊號量操作。當 S≥1 時,允許多個程序進入某特定區;當 S 變為 0 後,將阻止任何程序進入特定區。換言之,它相當於一個可控開關。
上面的格式為(s,t,d)也就是第一個為訊號量,第二個為限制,第三個為需求量
所以Swait(S,1,0)可以用做開關,只要S>=1,>=1時可分配,每次分配0個,所以只要S>=1,永遠都進的來,一旦S<1,也就是0往後,那麼就不滿足條件,就一個都進不去
小結
臨界區機制通過演算法控制程序序列進入臨界區,而訊號量機制則是藉助於原語操作(原子性)對臨界資源進行訪問控制
按照各種訊號量機制對應的規則以及相應的原語操作,就可以做到對資源的共享同步訪問,而不會出現問題
訊號量機制總共有四種
整型訊號量機制可以處理同一共享資源中,資源數目不止一個的情況
記錄型訊號量對整型訊號量機制的“忙等”進行了優化,通過block以及weakup原語進行阻塞和通知
AND型訊號量機制解決了對於多個共享資源的同步
訊號量集是對AND的再一次優化,既能夠處理多個共享資源同步的問題,還能夠設定資源申請的下限,是一種更加通用的處理方式
訊號量的應用
實現資源互斥訪問
為使多個程序能互斥地訪問某臨界資源,只須為該資源設定一互斥訊號量 mutex,並設其初始值為1
然後將各程序訪問該資源的臨界區 CS置於 wait(mutex)和 signal(mutex)操作之間即可。
這樣,每個欲訪問該臨界資源的程序在進入臨界區之前,都要先對 mutex 執行wait操作,若該資源此刻未被訪問,本次wait操作必然成功,程序便可進入自己的臨界區,否則程序將會阻塞。
步驟:
.......
wait(mutex);
臨界區
signal(mutex);
剩餘區
.......
實現前趨關係
前驅關係就是指執行順序,比如要求語句S1執行結束之後才能執行語句S2
在程序P1中:
S1;
signal(S);
在程序P2中
wait(S);
S2;
顯然,初始時將資源S設定為0,S2需要獲取到資源才會執行,而S1執行後就會釋放資源
對於一個更加複雜的前驅關係圖,如何實現?
從圖中可以看得出來,有S2和S3依賴S1
S4 和 S5依賴S2,而S6依賴S3、S4、S5
所以,S1應該提供兩個訊號量,提供給S2和S3 使用
S2 應該等待S1的訊號量,並且提供兩個訊號量給S4 和 S5
S3 應該等待S1的訊號量,並且提供一個訊號量給S6
S4應該等待S2的訊號量,並且提供一個給S6
S5應該等待S2的訊號量,並且提供一個給S6
S6應該等待S3、S4、S5
所以總共需要2+2+1+1+1 6個訊號量,我們取名為a,b,c,d,e,f,g
那麼過程如下:
Var a,b,c,d,e,f,g:semaphore: =0,0,0,0,0,0,0; P1{ S1; signal(a); signal(b); } P2{ wait(a); S2; signal(c); signal(d); } P3{ wait(b); S3; signal(e); } P4{ wait(c); S4; signal(f); } P5{ wait(d); S5; signal(g); } P6{ wait(e); wait(f); wait(g); S6; }
有人可能會疑惑,這裡的wait和signal又是什麼?他就是訊號量機制中的 wait 和 signal,他的內容是相當於下面的這些
同步方式之管程
雖然訊號量機制是一種既方便、又有效的程序同步機制 , 但每個要訪問臨界資源的程序都必須自備同步操作 wait(S)和 signal(S),這就使大量的同步操作分散在各個程序中。
這不僅 給系統的管理帶來了麻煩,而且還會因同步操作的使用不當而導致系統死鎖 。
在解決上述問題的過程中,便產生了一種新的程序同步工具——管程(Monitors)。
所以管程也可以這麼理解:它能夠確保臨界資源的同步訪問,並且還不用將大量的同步操作分散到各個程序中。
管程的定義
系統中的各種硬體資源和軟體資源,均可用 資料結構 抽象地描述其資源特性
比如一個IO裝置,有狀態(空閒還是忙時?),以及可以對他採取的操作(讀取還是寫入?)以及等待該資源的程序佇列來描述,所以就可以從這三個維度抽象描述一個IO裝置,而不關注他們的內部細節
又比如,一個集合,可以使用集合大小,型別,以及一組可執行操作來描述,比如Java中的ArrayList,有屬性,還有方法
可以把對該共享資料結構實施的操作定義為一組 過程
比如資源的請求和釋放過程定義為request 和 release
程序對共享資源的申請、釋放和其它操作,都是通過這組過程對共享資料結構的操作來實現的
類比到JavaBean的話,這些操作就如同setter和getter方法,所有對於指定物件的操作訪問都需要通過getter和setter方法 ,類似,所有對共享資料結構實施的操作,都需要藉助於這一組過程。
這組過程還可以根據資源的情況,或接受或阻塞程序的訪問,確保每次僅有一個程序使用共享資源,這樣就可以統一管理對共享資源的所有訪問,實現程序互斥
還是類比到JavaBean,就是相當於又增加了幾個方法,這些方法提供了更多的邏輯判斷控制
代表共享資源的資料結構,以及由對該共享資料結構實施操作的一組過程所組成的資源管理程式,共同構成了一個作業系統的資源管理模組,我們稱之為管程。
Dijkstra於1971年提出:
把所有程序對某一種臨界資源的同步操作都集中起來,構成一個所謂的祕書程序。
凡要訪問該臨界資源的程序,都需先報告祕書,由祕書來實現諸程序對同一臨界資源的互斥使用。
管程的概念經由Dijkstra提出的概念演化而來,由Hoare和Hanson於1973年提出。
定義如下 :
一組相關的資料結構和過程一併稱為管程。
Hansan的定義:一個管程定義了一個數據結構和能為併發程序在該資料結構上所執行的一組操作,這組操作能同步程序和改變管程中的資料。
所以管程的核心部分是對共享資料抽象描述的 資料結構 以及可以對該資料結構實施操作的一組 過程 。
使用資料結構對共享資源進行抽象描述,那麼必然要初始化資料,比如一個佇列Queue,有屬性size,這是一個抽象的資料結構,那麼一個具體的佇列到底有多大?你需要設定size 的值,所以對於管程還包括 初始化
一個基本的管程定義如下:
Minitor{ 管程內部的變數結構以及說明; 函式1(){ } ...... 函式N(){ } init(){ 對管程中的區域性變數進行初始化; } }
管程特點
管程就是管理程序,管程的概念就是設計模式中“依賴倒置原則”,依賴倒置原則是軟體設計的一個理念,IOC的概念就是依賴倒置原則的一個具體的設計
管程將對共享資源的同步處理封閉在管程內,需要申請和釋放資源的程序呼叫管程,這些程序不再需要自主維護同步。有了管程這個大管家(門面模式?)程序的同步任務將會變得更加簡單。
管程是牆,過程是門,想要訪問共享資源,必須通過管程的控制(通過城牆上的門,也就是經過管程的過程)
而管程每次只准許一個程序進入管程,從而實現了程序互斥
管程的核心理念就是相當於構造了一個管理程序同步的“IOC”容器。
管程是一個語言的組成成分(非作業系統支援部分),管程的互斥訪問完全由編譯程式在編譯時自動新增上,無需程式設計師關心,而且保證正確
一般的 monitor 實現模式是程式語言在語法上提供語法糖,而如何實現 monitor 機制,則屬於編譯器的工作
比如,Java中使用synchronized時,這是不是一種管程理念?你只是寫了一個synchronized關鍵字(語法糖),多執行緒的共享同步完全不用你操心了
(注意: 並不是所有的語言都支援管程的概念 )
條件變數
管程可以保證互斥,同一時刻僅有一個程序進入管程,所以他必然需要同步工具,如兩個同步操作原語 wait和 signal,他還需要互斥量用以控制管程進入的同步
當某程序通過管程請求獲得臨界資源而未能滿足時,管程便呼叫 wait 原語使該程序等待,並將其排在等待佇列上
僅當另一程序訪問完成並釋放該資源之後,管程呼叫signal原語,喚醒等待佇列中的隊首程序
但是,僅僅這個互斥量是不夠的
比如,如果需要處理之前提到過的“執行順序控制”,如何控制前驅關係?
當一個程序呼叫了管程,在管程中時被阻塞或掛起,直到阻塞或掛起的原因解除,而在此期間,如果該程序不釋放管程,則其它程序無法進入管程,被迫長時間地等待。
所以還需要其他的訊號量用於針對其他條件進行同步,這些就是條件變數,所以一個完整的管程定義為:
Minitor{ 管程內部的變數結構以及說明; condition條件變數列表; 函式1(){ } ...... 函式N(){ } init(){ 對管程中的區域性變數進行初始化; } }
條件變數就是當呼叫管程過程的程序無法執行時,用於阻塞程序的一種訊號量
管程中對每個條件變數都須予以說明,其形式為:Var x,y:condition。
對條件變數的操作僅僅是wait和signal,條件變數也是一種抽象資料型別,每個條件變數儲存了一個連結串列,用於記錄因該條件變數而阻塞的所有程序,同時提供的兩個操作即可表示為 x.wait和x.signal,其含義為:
① x.wait:正在呼叫管程的程序因 x 條件需要被阻塞或掛起,則呼叫 x.wait 將自己插入到x條件的等待佇列上,並釋放管程,直到x條件變化。此時其它程序可以使用該管程。
② x.signal:正在呼叫管程的程序發現 x 條件發生了變化,則呼叫 x.signal,重新啟動一個因 x 條件而阻塞或掛起的程序。如果存在多個這樣的程序,則選擇其中的一個,如果沒有,則繼續執行原程序,而不產生任何結果。這與訊號量機制中的 signal操作不同,因為後者總是要執行s:=s+1操作,因而總會改變訊號量的狀態。
如果有程序Q因x條件處於阻塞狀態, 當正在呼叫管程的程序P執行了x.signal操作後,程序Q 被重新啟動,此時兩個程序 P和Q,如何確定哪個執行,哪個等待,可採用下述兩種方式之一進行處理:
(1) P等待,直至Q 離開管程或等待另一條件。
(2) Q等待,直至P離開管程或等待另一條件。
採用哪種處理方式,當然是各執一詞。
Hoare 採用了第一種處理方式
而 Hansan 選擇了兩者的折衷,他規定管程中的過程所執行的signal 操作是過程體的最後一個操作,於是,程序P執行signal操作後立即退出管程,因而程序Q馬上被恢復執行。
總結
程序控制是作業系統的一種硬性管理,是必須要有的,如果沒有程序控制,就沒辦法合理安排執行程序, 根本無法完成狀態的切換維護等。
程序的同步是一種軟邏輯上的,如果不做,並不會導致系統出問題或者程序無法執行,但是如果不進行同步,結果卻很可能是錯誤的,所以也是必須做的
類比裝修的話,程序控制就是硬裝,不裝沒法住,總歸要水電搞搞好,程序同步就是傢俱家電和軟裝,硬裝後住進去不會出現“生存問題”(至少有水喝有電用),但是你要是連個熱水壺都沒有是打算要喝涼水麼
程序同步的概念多很複雜抽象,因為畢竟是概念表述,沒有涉及到具體的實現細節。
程序同步的核心是對於臨界資源的訪問控制,也就是對於臨界區的訪問。
不管是臨界區演算法還是訊號量機制還是管程機制,終歸也都是控制進入臨界區的不同的實現思路。
每種不同的演算法、機制都各自有各自的特色,場景。
訊號量機制將臨界資源的訪問的互斥,演化為可以多個程序訪問資源(整型訊號量),記錄型訊號量對整型訊號量機制進行優化,處理了忙等的問題
然後繼續演化出AND型,可以對不同的資源進行同步,而不僅僅是同一種資源
最後發展為訊號量集的形式,可以對不同的資源、不同的個數、不同的限制進行處理,變得更為通用。
管程更多的是一種設計思維,管程就是管理程序的程式,程序對於資源的同步操作全都依賴管程這一“大管家”,管程是程式語言級別的,不需要程式設計師進行過多的處理,一般會提供語法糖
需要注意並不是所有的語言都有管程的概念(Java是有的),管程讓你從同步的細節中解放出來,可以在很多場景下簡化同步的實現。
管程的概念是“執行緒同步”的“IOC”,大大簡化了同步的代價。
不管臨界區演算法還是訊號量機制還是藉助於管程,他們都是一種同步工具,可以認為他們就是一組“方法”,“方法”的邏輯就是本文前面介紹的原理
在需要程序同步問題的解決思路中,可以直接使用“封裝好的方法”
以上,儘管都是在說作業系統關於程序的同步的處理,其實,在同步問題上程序和執行緒的設計理念是相通的
因為這本質上都是在說併發----多道程式執行的作業系統,通常使用輪轉時間片的方式併發的執行多個程序
原文地址: 程序同步概念簡介 多執行緒上篇(四)