以下羅列一些在多道程序環境下,產生的一系列經典的進程同步問題。
生產者-消費者問題
問題描述
生產者-消費者(producer-consumer)問題是有一群生產者進程在生產產品,並將這些產品提供給消費者進程去消費。在兩者之間設置了一個具有 n 個緩沖區的緩沖池,生產者進程將其所生產的產品放入一個緩沖區中,消費者進程可從一個緩沖區中取走產品去消費。只有緩沖區沒滿時,生產者才能把產品放入緩沖區,否則必須等待。只有緩沖區不空時,消費者才能從中取出產品,否則必須等待。緩沖區是臨界資源,各進程必須互斥地訪問。
可以利用一個數組 buffer 來表示具有 n 個緩沖區的緩沖池,每投入或取出一個產品時,緩沖池 buffer 中暫存產品或或被取走產品的數組單元指針 in 或 out 需要移動,這些用代碼描述如下。
int in = 0; //輸入指針
int out = 0; //輸出指針
item buffer[n]; //緩沖區
由於 buffer 描述的緩沖池是循環隊列結構,因此輸入指針 in 或輸出指針 out 表示成“in = (in + 1) % n” 和 “out = (out + 1) % n”,當 (in + 1) % n = out 時表示緩沖池滿,in = out 則表示緩沖池空。
記錄型信號量解法
可利用互斥信號 mutex 實現諸進程對緩沖池的互斥使用,利用信號量 empty 和 full 分別表示緩沖池中空緩區和滿緩沖區的數量。
semaphore mutex = 1; //互斥信號量,實現對緩沖區的互斥訪問
semaphore empty = n; //同步信號量,表示空閑緩沖區的數量
semaphore full = 0; //同步信號量,表示非空緩沖區的數量
又假定這些生產者和消費者相互等效,只要緩沖池未滿,生產者可將消息送入緩沖池.只要緩沖池未空,消費者便可從緩沖池中取走一個消息。應注意每個程序中用於實現互斥的 wait(mutex) 和 signal(mutex) 必須成對地出現,其次對資源信號量 empty 和 full 的 wait 和 signal 操作。
對生產者而言,可以用代碼描述如下:
void proceducer(){
do{
producer an item nextp; //生產一個產品
wait(empty); //消耗一個空閑緩沖區
/*實現互斥*/
wait(mutex);
buffer[in] = nextp; //產品放入緩沖區
in = (in + 1) % n; //移動輸入指針
signal(mutex);
/*實現互斥*/
signal(full);
}while(TRUE);
}
對消費者而言,可以用代碼描述如下:
void consumer(){
do{
wait(full); //消耗一個產品
/*實現互斥*/
wait(mutex);
nextc = buffer[out]; //產品拿出緩沖區
out = (out + 1) % n; //移動輸出指針
signal(mutex);
/*實現互斥*/
signal(empty); //增加一個空閑緩沖區
consumer the item in nextc; //使用產品
}while(TRUE);
}
整個生產消費者問題的流程,用代碼描述如下:
void main() {
cobegin
proceducer();
consumer();
coend
}
AND 信號量解法
利用 AND 信號量來解決時,用 Swait(empty,mutex) 來代替 wait(empty) 和 wait(mutex),用 Ssignal(mutex, full) 來代替 signal(mutex) 和 signal(full)。用 Swait(full,mutex)代替 wait(full) 和 wait(mutex),以及用 Ssignal(mutex,empty) 代替 Signal(mutex) 和 Signal(empty)。利用 AND 信號量來解決生產者-消費者問題的代碼描述如下:
int in = 0; //輸入指針
int out = 0; //輸出指針
item buffer[n]; //緩沖區
semaphore mutex = 1; //互斥信號量,實現對緩沖區的互斥訪問
semaphore empty = n; //同步信號量,表示空閑緩沖區的數量
semaphore full = 0; //同步信號量,表示非空緩沖區的數量
void proceducer(){
do{
producer an item nextp;
Swait(empty, mutex);
buffer[in] = nextp;
in = (in + 1) % n;
Ssignal(mutex, full);
}while(TRUE);
}
void consumer(){
do{
Swait(full, mutex);
nextc= buffer[out];
out = (out + 1) % n;
Ssignal(mutex, empty);
consumer the item in nextc;
}while(TRUE);
}
void main() {
cobegin
proceducer();
consumer();
coend
}
管程解法
利用管程來解決生產者-消費者問題時,首先便是為它們建立一個管程,並命名 為procducerconsumer(PC)。用整型變量 count 來表示在緩沖池中已有的產品數目,其中包括兩個過程:
過程 | 說明 |
---|---|
put(x) | 生產者利用該過程將自己生產的產品投放到緩沖池中 |
get(x) | 消費者利用該過程從緩沖池中取出一個產品 |
對於條件變量 notfull 和 notempty,分別有兩個過程 cwait 和 csignal 對它們進行操作: | |
過程 | 說明 |
--- | --- |
cwait(condition) | 當管程被一個進程占用時,其他進程調用該過程時阻塞,並掛在條件 condition 的隊列上 |
csignal(condition) | 喚醒在 cwait 執行后阻塞在條件 condition 隊列上的進程 |
PC 管程可描述如下: |
Monitor procducerconsumer {
int in = 0; //輸入指針
int out = 0; //輸出指針
item buffer[n]; //緩沖區
condition notfull, notempty; //條件變量
int count = 0; //緩沖池中已有的產品數目
public void put(item x){
if(count >= N) //緩沖池已滿
{
cwait(notfull); //生產者等待
}
buffer[in] = x;
in = (in + 1) % N;
count++;
csignal(notempty);
}
public void get(item x){
if (count<= 0) //緩沖池沒有可用的產品
{
cwait(notempty); //消費者等待
}
x = buffer[out];
out =(out+1) % N;
count--;
csignal(notfull);
}
}PC;
在利用管程解決生產者-消費者問題時,可用代碼描述為:
void producer(){
item x;
while(TRUE){
produce an item in nextp;
PC.put(x);
}
}
void consumer(){
item x;
while(TRUE){
PC.get(x);
consume the item in nextc;
}
}
void main() {
cobegin
proceducer();
consumer();
coend
}
哲學家進餐問題
問題描述
一張圓桌上坐着 5 名哲學家,每兩個哲學家之間的桌上擺一根筷子,桌子的中間是一碗米飯。哲學家只做思考和進餐兩件事情,哲學家在思考時不影響他人,只有當哲學家飢餓時才試圖拿起左、右兩根筷子(一根一根地拿起)。如果筷子已在他人手上則需等待,飢餓的哲學家只有同時拿起兩根筷子才可以開始進餐,當進餐完畢后,放下筷子繼續思考。
解法
經分析可知,放在桌子上的筷子是臨界資源,在一段時間內只允許一位哲學家使用。為了實現對筷子的互斥使用,可以用一個信號量表示一只筷子,由這五個信號量構成信號量數組。
semaphore chopstick[5] = {1,1,1,1,1};
所有信號量均被初始化為 1,當哲學家飢餓時總是先去拿他左邊的筷子,成功后再去拿他右邊的筷子便可進餐。進餐完畢時先放下他左邊的筷子,然后再放他右邊的筷子。
do{
wait(chopstick[i]); //拿起左邊的筷子
wait(chopstick[(i + 1) % 5]); //拿起右邊的筷子
eat
signal(chopstick[i]); //放下左邊的筷子
signal(chopstick[(i + 1) % 5]); //放下右邊的筷子
think
}while(TRUE);
除了利用記錄型信號量,也可以使用 AND 型信號量來解決,這樣的寫法更為簡潔。
do{
Sswait(chopstick[(i + 1) % 5], chopstick[i]); //拿起筷子
eat
Ssignal(chopstick[(i+1)%5],chopstick[i]); //放下筷子
think
}while(TRUE);
可能的死鎖
假如五位哲學家同時飢餓而各自拿起左邊的筷子時,就會使五個信號量 chopstick 均為 0,當他們再試圖去拿右邊的筷子時,都將因無筷子可拿而無限期地等待。對於這樣的死鎖問題,可采取以下幾種解決方法:
- 至多允許有四位哲學家同時去拿左邊的筷子,最終能保證至少有一位哲學家能夠進餐,並在用畢時能釋放出他用過的兩只筷子;
- 僅當哲學家的左、右兩只筷子均可用時,才允許他拿起筷子進餐。
- 奇數號哲學家先拿他左邊的筷子,然后再去拿右邊的筷子,而偶數號哲學家則相反。按此規定將是 1、2 號哲學家競爭 1 號筷子,3、4 號哲學家競爭 3 號筷子。即五位哲學家都先競爭奇數號筷子,獲得后再去競爭偶數號筷子,最后總會有一位哲學家能獲得兩只筷子而進餐。
讀者-寫者問題
問題描述
有讀者和寫者兩組並發進程,共享一個文件,當兩個或兩個以上的讀進程同時訪問共享數據時不會產生副作用。但若某個 Reader 進程和其他進程(Reader 進程或 writer 進程)同時訪問共享數據時,則可能導致數據不一致的錯誤。因此要求:
- 允許多個 Reader 可以同時對文件執行讀操作;
- 只允許一個 writer 往文件中寫信息;
- 任一 writer 在完成寫操作之前不允許其他 Reader 或 writer 工作;
- writer 執行寫操作前,應讓已有的 Reader 者和 writer 全部退出。
記錄型信號量解法
為實現 Reader 與 Writer 進程間在讀或寫時的互斥,設置一個互斥信號量 Wmutex,再設置一個整型變量 Readcount 表示正在讀的進程數目。又因為 Readcount 是一個可被多個 Reader 進程訪問的臨界資源,因此也應該為它設置一個互斥信號量 rmutex。
semaphore rmutex = 1; //用於保證對 count 變量的互斥訪問
semaphore wmutex = 1; //用於實現對文件的互斥訪問,表示當前是否有進程在訪問共享文件
int readcount = 0; //記錄當前有幾個讀進程在訪問文件
對 reader 而言,可以用代碼描述如下:
void reader(){
do{
wait(rmutex); //reader 進程互斥訪問 readcount
if(readcount == 0) //第一個 reader 進程開始讀
{
wait(wmutex); //給共享文件“加鎖”
}
readcount++; //訪問文件的 reader 進程數加 1
signal(rmutex);
perform read operation; //讀文件
wait(rmutex); //各個 reader 進程互斥訪問 readcount
readcount--; //訪問文件的 reader 進程數減 1
if(readcount == 0)
{
signal(wmutex); //最后一個 reader 進程“解鎖”
}
signal(rmutex);
}while(TRUE);
}
對 Writer 而言,可以用代碼描述如下:
void writer()
{
do{
wait(wmutex); //寫之前“加鎖”
perform write operation;
signal(wmutex); //寫之后“解鎖”
}while(TRUE);
}
對於整個讀者-寫者問題過程,可以用代碼描述如下:
void main() {
cobegin
reader();
writer();
coend
}
信號量集機制解法
此時讀者一寫者問題引入一個限制,最多只允許 RN 個讀者同時讀,為此又引入了一個信號量 L,並賦予其初值為 RN。通過執行 wait(L, 1, 1) 操作來控制讀者的數目,每當有一個讀者進入時,就要先執行 wait(L,1,1) 操作,使 L 的值減 1。當有 RN 個讀者進入讀后,L 便減為 0,第 RN + 1 個讀者要進入讀時,必然會因 wait(L,1,1) 操作失敗而阻塞。
int RN; //最多允許同時讀取文件的 reader 進程數
semaphore L = RN; //保證最多只有 RN 個 reader 進程同時讀
semaphore mx = 1; //標志是否有 writer 進程在操作文件
void reader(){
do{
Swait(L, 1, 1); //增加一個 reader 進程讀文件
Swait(mx, 1, 0); //無 writer 進程寫文件
perform read operation;
Ssignal(L, 1); //減少一個正在讀文件的 reader 進程
}while(TRUE);
}
void writer(){
do{
Swait(mx, 1, 1; L, RN, 0) //無 reader 或 writer 進程在操作,“加鎖”
perform write operation;
Ssignal(mx, 1); //writer 進程“解鎖”
}while(TRUE);
}
void main(){
cobegin
reader();
writer();
coend
}
吸煙者問題
問題描述
假設一個系統有三個抽煙者進程和一個供應者進程,每個抽煙者不停地卷煙並抽掉它,但是要卷起並抽掉一支煙需要有三種材料:煙草、紙和膠水。三個抽煙者中第一個擁有煙草,第二個擁有紙、第三個擁有膠水。供應者進程無限地提供三種材料,供應者每次將兩種材料放桌子上,擁有剩下那種材料的抽煙者卷一根煙並抽掉它,並給供應者進程一個信號告訴完成了,供應者就會放另外兩種材料再桌上,這個過程一直重復。
解法
從事件的角度來分析,吸煙者問題有 4 個同步關系,分別是桌上有組合一時第一個抽煙者取走東西,桌上有組合二時第二個抽煙者取走東西,桌上有組合三時第三個抽煙者取走東西,最后是吸煙者發出完成信號,供應者將下一個組合放到桌上。因此需要設置 4 個信號量,來分別對應 4 個同步關系。
semaphore offerl = 0; //桌上組合一的數量
semaphore offer2 = 0; //桌上組合二的數量
semaphore offer3 = 0; //桌上組合三的數量
semaphore finish = 0; //抽煙是否完成
int i = 0; //正在吸煙的吸煙者序號
對於材料提供者而言,可以用代碼描述如下:
void provider(){
while(1){
if(i == 0){
將組合一放桌上;
wait(offer1);
}
else if(i == l){
將組合二放桌上;
wait(offer2);
}
else if(i == 2){
將組合三放桌上;
wait(offer3);
}
i = (i + 1) % 3;
signal(finish);
}
}
對於 3 位吸煙者,可以用代碼描述如下:
void smoker1(){
while(1){
signal(offer1);
從桌上拿走組合一,卷煙抽;
wait(finish);
}
}
void smoker2(){
while(1){
signal(offer2);
從桌上拿走組合二,卷煙抽;
wait(finish);
}
}
void smoker3(){
while(1){
signal(offer3);
從桌上拿走組合三,卷煙抽;
wait(finish);
}
}
參考資料
《計算機操作系統(第四版)》,湯小丹 梁紅兵 哲鳳屏 湯子瀛 編著,西安電子科技大學出版社