問題描述
系統中有一組生產者進程和一組消費者進程,生產者進程每次生產一個產品放入緩沖區,消費者進程每次從緩沖區取出一個產品並使用;緩沖區在同一時刻只能允許一個進程訪問。
問題分析
- 生產者、消費者共享一個初始為空、大小為n的緩沖區,我們把緩沖區中未存放數據的一個塊,當作一個“空位”;把其中按塊存放的數據當作“產品”。
- 同步關系:生產者與消費者
- 只有緩沖區有空位時,生產者才能把產品放入緩沖區
- 生產者把“空位”當作資源,緩沖區初始為空,即空位數量為n(n:空的緩沖區大小)
- 所以可以設置信號量empty,初始n
- empty>0時,有空位,生產者可以消耗“空位”這種資源即P(empty);
- empty<=0時,生產者無“空位”資源可用,便會掛起到阻塞隊列等待。
- 所以可以設置信號量empty,初始n
- 生產者把“空位”當作資源,緩沖區初始為空,即空位數量為n(n:空的緩沖區大小)
- 只有緩沖區有產品時,消費者才能從緩沖區中取出產品
- 消費者把“產品”當作資源,緩沖區初始為空,即產品數量為0
- 所以可以設置信號量full,初始為0
- full<=0,消費者無“產品”這種資源可用,便會掛起到阻塞隊列
- full>0,有產品,消費者可以消耗“產品”這種資源即P(full)
- 所以可以設置信號量full,初始為0
- 消費者把“產品”當作資源,緩沖區初始為空,即產品數量為0
- 進一步:
- empty>0時,有空位,生產者可以消耗“空位”這種資源即P(empty)的同時:生產了“產品這種資源”,即V(full);
- full>0時,有產品,消費者可以消耗“產品”這種資源即P(full)的同時:生產了“空位”這種資源,即V(empty);
- 只有緩沖區有空位時,生產者才能把產品放入緩沖區
- 互斥關系:所有進程之間
- 緩沖區是臨界資源,各個進程必須互斥地訪問
- 設置信號零mutex,初始為1
- 在進入區P(mutex)申請資源
- 在退出區V(mutex)釋放資源
- 設置信號零mutex,初始為1
- 緩沖區是臨界資源,各個進程必須互斥地訪問

設計

typedef struct{
int value;
Struct process *L; //等待序列
}semaphore;
semaphore full;
semaphore empty;
semaphore mutex;
//某進程需要使用資源時,通過wait原語申請:P操作
void wait(semaphore S){
S.value--;
if(S.value < 0){
block(S.L);//阻塞原語,將當前進程掛載到當前semaphore的阻塞隊列
}
}
//進程使用完資源后,通過signal原語釋放:V操作
void signal(semaphore S){
S.value++;
if(S.value <= 0){
wakeup(S.L);//喚醒原語,將當前semaphore的阻塞隊列中的第一個進程喚醒
}
}
full.value=0; //緩沖區 “產品”資源數(初始為0),用於實現生產者與消費者進程的同步
empty.value=n; //緩沖區 “空位”資源數(初始為n),用於實現生產者與消費者進程的同步
mutex.value=1; //互斥信號量,用於實現所有進程之間互斥地訪問緩沖區
//生產者
producer(){
while(1){
Produce(); //生產“產品”
P(empty); //“空位”數-1
P(mutex); //臨界區上鎖
Storage(); //擺放產品
V(mutex); //臨界區解鎖
V(full); //“產品”數+1
}
}
//消費者
consumer(){
while(1){
P(full); //“產品”數-1
P(mutex); //臨界區上鎖
TakeOut(); //拿走產品
V(mutex); //臨界區解鎖
V(empty); //“空位”數+1
Use(); //使用“產品”
}
}
對於各個操作順序的理解:
-
對於一部分操作的順序,我們很好理解,符合我們的認知:
-
//生產者 Produce(); //生產產品 P(empty); //“空位”數-1,可能有人在這里會問這個操作為什么不可以放在“Storage甚至是V(full)”后面,考慮一種情況:當空位數為0時,我們是不能擺放產品的,而這個操作正是在檢查是否還有“空位”這種資源;所以它一定在Storage前面。 Storage(); //擺放產品 V(full); //“產品”數+1,可能有人在這里會問這個操作為什么不可以放在“Storage甚至是P(empty)”前面,考慮一種情況:當產品數為n時,我們是不能再擺放產品的,因為緩沖區已滿,再向其中添加數據(執行Storage)是要出問題的;所以它一定在Storage后面。 -
//消費者 P(full); //“產品”數-1,同上面一樣,可能有人在這里會問這個操作為什么不可以放在“TakeOut甚至是V(empty)”后面,考慮一種情況:當產品數為0時,我們是不能拿走產品的,而這個操作正是在檢查是否還有“產品”這種資源;所以它一定在TakeOut前面。 TakeOut(); //拿走產品 V(empty); //“空位”數+1,同上面一樣,可能有人在這里會問這個操作為什么不可以放在“TakeOut甚至是P(full)”前面,考慮一種情況:當空位數為n時,我們是不能拿走產品的,因為緩沖區已經空了,再拿走(執行TakeOut)拿走個寂寞;所以它一定在TakeOut前面。 Use(); //使用“產品”
-
-
對於其他操作:實現同步的P操作一定要在實現互斥的P操作之前,為什么呢?
-
反向分析:我們考慮若調換生產者上述兩個P操作的順序:
//生產者 producer(){ while(1){ Produce(); //生產產品 P(mutex); //臨界區上鎖 P(empty); //“空位”數-1 Storage(); //擺放產品 V(mutex); //臨界區解鎖 V(full); //“產品”數+1 } } //消費者 consumer(){ while(1){ P(full); //“產品”數-1 P(mutex); //臨界區上鎖 TakeOut(); //拿走產品 V(mutex); //臨界區解鎖 V(empty); //“空位”數+1 Use(); //使用“產品” } }- 若此時緩沖區已經放滿產品,則empty=0,full=n
- 生產者進程執行P(mutex),mutex變為0,由於empty為0即沒有“空位”了,需要生產者進程阻塞,等待消費者拿走產品
- 切換至消費者進程,消費者進程執行到P(mutex),由於mutex為0即生產者未釋放臨界資源,需要生產者釋放臨界資源,消費者進程阻塞
- 互相等待,進入死鎖
- 結論:不要讓因同步引起的進程阻塞(P操作可能產生結果)發生在為臨界區上鎖之后,因為:
- 臨界區上鎖,表示臨界資源已被占用;若對臨界區未解鎖之前,發生了因同步引起的進程阻塞(上例中即需要生產者進程阻塞,等待消費者拿走產品)。
- 那么緊接着切換到另一個和此進程有同步和互斥關系的進程運行,且該進程也要對臨界區訪問:由於臨界區已被上鎖,則兩進程進入死鎖狀態。
- 若此時緩沖區已經放滿產品,則empty=0,full=n
-
說白了,對於進程來說,阻塞前對臨界資源上鎖,就是占着茅坑不拉屎(粗俗的講:死鎖就是兩個人都占着茅坑不拉屎,還都等着對方離開然后在別人茅坑里才能拉)。
-
-
Produce和Use可以放在臨界區嗎?
- 為了進程快速交替使用臨界資源,要讓臨界區代碼盡量短,所以不把生產和使用產品的代碼放在臨界區代碼中
-
V操作不會使進程阻塞,故互斥和同步的V操作順序從理論上來說可以調換,但同3一樣,為了進程快速交替使用臨界資源,要讓臨界區代碼盡量短,所以不把同步的V操作放在臨界區代碼中
-
綜上:不要往臨界區中添加與訪問臨界資源無關的操作/代碼

