進程的同步與互斥之生產者消費者問題:對信號量設置的理解及PV操作順序分析


問題描述

系統中有一組生產者進程和一組消費者進程,生產者進程每次生產一個產品放入緩沖區,消費者進程每次從緩沖區取出一個產品並使用;緩沖區在同一時刻只能允許一個進程訪問。

問題分析

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

設計

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();		//使用“產品”
    }
}

對於各個操作順序的理解:

  1. 對於一部分操作的順序,我們很好理解,符合我們的認知:

    •  //生產者
       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();		//使用“產品”
      
  2. 對於其他操作:實現同步的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操作可能產生結果)發生在為臨界區上鎖之后,因為:
        1. 臨界區上鎖,表示臨界資源已被占用;若對臨界區未解鎖之前,發生了因同步引起的進程阻塞(上例中即需要生產者進程阻塞,等待消費者拿走產品)。
        2. 那么緊接着切換到另一個和此進程有同步和互斥關系的進程運行,且該進程也要對臨界區訪問:由於臨界區已被上鎖,則兩進程進入死鎖狀態
    • 說白了,對於進程來說,阻塞前對臨界資源上鎖,就是占着茅坑不拉屎(粗俗的講:死鎖就是兩個人都占着茅坑不拉屎,還都等着對方離開然后在別人茅坑里才能拉)。

  3. Produce和Use可以放在臨界區嗎?

    • 為了進程快速交替使用臨界資源,要讓臨界區代碼盡量短,所以不把生產和使用產品的代碼放在臨界區代碼中
  4. V操作不會使進程阻塞,故互斥和同步的V操作順序從理論上來說可以調換,但同3一樣,為了進程快速交替使用臨界資源,要讓臨界區代碼盡量短,所以不把同步的V操作放在臨界區代碼中

  5. 綜上:不要往臨界區中添加與訪問臨界資源無關的操作/代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM