程序同步(二)——訊號量機制(整型、記錄型)
訊號量機制
訊號量(Semaphore)機制是一種卓有成效的程序同步工具。
整型訊號量
把整型訊號量定義為一個表示資源數目的整型量S
,除初始化外,僅能通過兩個標準的原子操作wait(S)
和signal(S)
來訪問。
wait(S)和signal(S)操作可以描述為:
wait(S): while S <= 0 do no-op;
S:=S-1;
signal(S): S:=S+1;
wait(S)和signal(S)是兩個原子操作,它們在執行時是不可中斷的。當一個程序在修改某訊號量時,沒有其他程序能同時對該訊號量進行修改。
在整型訊號量機制中的wait操作中,只要是訊號量S<=0,就會不斷地測試。因此該機制沒有遵循“讓權等待”準則,而是使程序處於“忙等”的狀態。
記錄型訊號量
記錄型訊號量機制採取了“讓權等待”策略,是一種不存在“忙等”現象的程序同步機制。記錄型訊號量時由於它採用了記錄型資料結果而得名的。在訊號量機制中,除了需要一個用於代表資源數目數的整型變數value
外,還需要一個程序連結串列指標L
用於連結所有等待的程序。
value
和L
兩個資料項可以描述為:
type semaphore=record
value: integer;
L: list of process;
end
記錄型訊號量的wait(S)和signal(S)操作可以描述為:
procedure wait(S)
var S: semaphore;
begin
S.value:=S.value-1;
if S.value < 0 then block(S.L);
end
procedure signal(S)
var S: semaphore;
begin
S.value:=S.value+1;
if S.value<=0 then wakeup(S.L);
end
S.value
的初值表示系統中某類資源的數目,被稱為資源訊號量。
對它每次wait操作,意味程序請求一個單位的該類資源,使得系統中可分配的該類資源數減少一個,因此描述為S.value:=S.value-1
;當S.value<0時,表示資源分配完畢,因此該訪問程序應呼叫block原語進行自我阻塞放棄處理機,並插入到訊號量連結串列S.L
S.value
的絕對值表示該訊號量連結串列中已阻塞程序的數目。可見該機制遵循了“讓權等待”準則。
對訊號量的每次signal操作表示執行程序釋放一個單位資源,使得系統中可供分配的該類資源數增加一個,因此表示為S.value:=S.value+1
。如果+1後仍然是S.value<=0,說明訊號量連結串列中仍然有等待該資源的程序被阻塞,因此還應呼叫wakeup原語將S.L連結串列中的第一個等待程序喚醒。
如果S.value的初值為1,表示允許一個程序訪問臨界資源,此時的訊號量轉化為互斥訊號量用於程序互斥。
利用記錄型訊號量解決生產者—消費者問題
設在生產者和消費者之間的公用緩衝池中有n個緩衝區,這時可以利用互斥訊號量mutex
實現各個程序對緩衝池的互斥作用。利用訊號量empty
和full
表示空緩衝區和滿緩衝區的數量。假定生產者和消費者相互等效,只要緩衝池未滿,生產者就可以將訊息送入緩衝池;只要緩衝池為空,消費者就可以從中取走一個資訊。
描述如下:
Var mutex, empty, full: semaphore:=1,n,0;
buffer:array[0,...,n-1] of item;
in, out: integer:=0, 0;
begin
parbegin
proceducer: begin
repeat
...
producer an item nextp;
...
wait(empty);
wait(mutex);
buffer(in):=nextp;
in:=(in+1) mod n;
signal(mutex);
signal(full);
until false;
end
consumer: begin
repeat
wait(full);
wait(mutex);
nextc:=buffer(out);
out:=(out+1) mod n;
signal(mutex);
signal(empty);
consumer the item in nextc;
until false;
end
parend
end
需要注意的是,在每個程式中用於實現互斥的wait(mutex)和signal(mutex)必須成對出現;對資源訊號量empty和full的wait和signal操作,同樣需要成對出現,但它們處在不同的程式中。
如果兩個wait操作互換位置,即wait(empty)和wait(metex)互換位置,或wai(full)和wait(mutex)互換位置,都可能因此死鎖。若signal(full)和signal(mutex)互換位置或者signal(empty)和signal(mutex)互換位置,則不會引起死鎖,只會改變臨界資源的釋放次序。
利用記錄型訊號解決哲學家進餐問題
在該問題中,放在桌子上的筷子是臨界資源,在一段時間內只允許一位哲學家使用。為了實現筷子的互斥使用,可以使用一個訊號量表示一隻筷子,這五個訊號量構成訊號陣列,描述如下:
Var chopstick: array[0, ..., 4] of semaphore;
所有的訊號量都被初始化為1,第i位哲學家的活動可以描述為:
repeat
wait(chopstick[i]);
wait(chopstick[(i+1) mod 5]);
...
eat;
...
signal(chopstick[i]);
signal(chopstick[(i+1) mod 5]);
...
think;
until false;
當哲學家飢餓時,總是先去拿他左邊的筷子wait(chopstick[i]),再去拿它右邊的筷子wait(chopstick[(i+1) mod 5]),都成功後便可進餐。用餐後,先放下左邊的筷子signal(chopstick[i]),再放下右邊的筷子signal(chopstick[(i+1) mod 5])。這種解法可以保證不會有兩個相鄰的哲學家同時進餐,但是可能會造成死鎖。如果五位哲學家同時飢餓而各自拿起左邊的筷子時,就會使5個訊號量chopstick都為0,當他們試圖去拿起右邊的筷子時,都將因為沒有筷子可拿而無線等待。
死鎖問題可以採取下面幾個辦法:
(1) 最多允許有4位哲學家去同時拿左邊的筷子,這樣能保證最後至少有一個哲學家能有進餐並且能在用餐後釋放出他使用的兩隻筷子從而使更多的哲學家能進餐。
(2) 僅當哲學家的左右兩隻筷子均可使用時,才允許他拿。
(3) 規定奇數號哲學家先拿他左邊的筷子,然後再拿右邊的筷子,偶數號的哲學家相反。
利用記錄型訊號量解決讀者—寫者問題
所謂“讀者—寫者問題(Reader-Writer Problem)”是指保證一個Writer程序必須與其他程序互斥地訪問共享物件的同步問題。不允許一個Writer程序和其他Reader程序或Writer程序同時訪問共享物件,因為這種訪問將會引起混亂。
為了實現Reader與Writer程序在讀或者寫時的互斥,設定一個互斥訊號量Wmutex。設定一個整型變數Readcount表示正在讀的程序數。只要有一個Reader程序在讀,便不允許Writer程序去寫。因此,僅當Readcount=0,表示尚無Reader程序在讀時,Reader程序才需要執行Wait(Wmutex)操作;僅當Reader程序執行了Readcount-1操作後其值為0時,才進行signal(Wmutex)操作。Readcount又是一個被多個Reader程序訪問的臨界資源,因此也應該為他設定一個互斥訊號量rmutex。
讀者—寫者問題可以描述如下:
Var rmutex, wmutex: semaphore:=1,1;
Readcount: integer:=0;
begin
parbegin
Reader: begin
repeat
wait(rmutex);
if readcount=0 then wait(wmutex);
Readcount:=Readcount+1;
signal(rmutex);
...
perform read operation;
wait(rmutex);
readcount:=readcount-1;
if readcount=0 then signal(wmutex);
signal(rmutex);
until false;
end
Writer: begin
repeat
wait(wmutex);
perform write operation;
signal(wmutex);
until false;
end
parend
end