本文主要內容:1)管程(Monitor)介紹;2)管程實現;3)管程應用
本文主要內容:
- 管程(Monitor)介紹
- 管程實現
- 管程應用
一、管程(Monitor)介紹
1.1 管程
前一篇文章介紹了信號量以及使用,信號量已經提供了一個方便且高效的進程同步機制,但是信號量有個缺點就是每次都需要程式員專門的去調用PV操作,如果程式員由於大意調用錯了PV操作,比如該調用P操作的時候卻調用了V操作,該針對X信號量調用P操作,卻對Y信號量調用了P操作。這種錯誤是非常危險的,因為進程同步的問題不是每次都能重現,比如前面的錯誤在測試環境可能不會出現錯誤,但是到了生產環境就有可能出現,且無法重現。
為瞭解決上述問題,引入了管程(Monitor),信號量是操作系統層面的結構,管程是一個程式結構,由編程語言封裝,最終由編譯器:
- 一個管程定義了一個數據結構和能夠併發進程所執行的一組操作,這組操作能同步進程和改變管程中的數據
- 進程可以調用管程的操作,但是不能訪問管程的內部數據結構
- 它表示一個對象自己維護自己的狀態,並且能夠根據自身狀態來同步併發的線程操作,而不是把這種同步的手段交給調用者來處理。
- 管程保證同一時間只有一個進程處在管程內部的方法內
monitor MonitorName
{
//shared variable declarations
procedure P1(...)
{
}
procedure P2(...)
{
}
procedure P3(...)
{
}
procedure PN(...)
{
}
initialization code(...)
{
}
}
1.2 條件(Condition variables)
只有管程是不夠的,在生產者消費者的例子中,當發現buffer中如果已經沒有item的時候,消費者如何block自己呢?介於這個原因,又引入了條件變數(Condition Variables)。條件變數同時提供了兩個方法:P以及V用於block和unblock。
二、管程實現
管程通過隊列來跟蹤那些嘗試著想訪問管程的進程,為了排他訪問,管程必須得有一個鎖,訪問管程的進程會得到這個鎖,其他嘗試訪問管程的進程就得block。被block的進程會被放入隊列,等待被unblock。
管程的實現中有如下的隊列:
- 進入隊列(Entry Queue),保存嘗試從外部訪問管程程式的進程,每個管程至少有一個進入隊列
- 被喚醒的隊列(Signaller Queue),保存剛剛執行了V操場的進程(signal)
- 等待隊列(Waiting Queue),保存剛剛被V操作喚醒的進程
- 條件變數隊列(Condition Variable Queue),保存剛剛執行了條件變數Wait(P)操作的進程
三、管程應用
上一篇文章針對哲學家用餐給出的解決方案有個問題:當所有哲學家同時拿起筷子想吃飯的時候就會發生死鎖,所有哲學家都會一直處於等待狀態。本文用管程給出瞭解決方案,哲學家吃飯得滿足的條件:
- 哲學家餓(hungry)
- 哲學家的左右鄰居都沒有吃飯
如果滿足上麵條件,則哲學家便可以拿起筷子進行用餐,否則哲學家就得等待
monitor dp
{
enum {thinking, hungry, eating} state[5];
condition self[5];
void pickup(int i) {
state[i] = hungry;
test(i);
if (state[i] != eating)
self[i].wait();//P操作
}
void putdown(int i) {
state[i] = thinking;
test( (i+4)%5 );
test( (i+1)%5 );
}
void test(int i) {
if ((state[(i+4)%5] != eating) &&
(state[i] == hungry) &&
(state[(i+1)%5] != eating)) {
state[i] = eating;
self[i].signal();//V操作
}
}
void init() {
for (int i = 0; i < 5; i++)
state[i] = thinking;
}
}
Condition Self代表五個哲學家,當不能吃時候進行wait。
db.pickup(i);//找筷子
...
eating//吃
...
dp.putdown(i);//放下筷子
管程更多的是對資源的併發訪問做了一次封裝,感覺有很多OO編程的思想,直接用信號量的話,各種控制會很發散,當然容易出錯,信號量本事是沒有問題的。但是使用容易出錯。且由於散髮在多出,出問題不好排查,也不好修複。
管程是一種編程思想,這種思想就是封裝,並且有DRY的感覺。