1. 程式人生 > >訊號量與生產者消費者問題

訊號量與生產者消費者問題

生產者—消費者問題

        生產者—消費者題型在各類考試(考研、程式設計師證書、程式設計師面試筆試、期末考試)很常見,原因之一是生產者—消費者題型在實際的併發程式(多程序、多執行緒)設計中很常見;之二是這種題型綜合性較好,涉及程序合作、互斥,有時還涉及死鎖的避免。生產者—消費者題型可以全面考核你對併發程式的理解和設計能力。

        生產者—消費者題型最基本的是有界緩衝區的生產者消費者問題和無界緩衝區的生產者消費者問題,對這兩個問題的解我們應該掌握其解決方案。

        對於有界緩衝區的生產者—消費者問題,兩個程序共享一個公共的固定大小的緩衝區。其中一個是生產者,將資訊放入緩衝區;另一個是消費者,從緩衝區中取出資訊(也可以把這個問題一般化為m個生產者和n個消費者問題,但是我們只討論一個生產者和一個消費者的情況,這樣可以簡化解決方案)。


        問題在於當緩衝區已滿,而此時生產者還想向其中放入一個新的資料項的情況,其解決辦法是讓生產者睡眠,待消費者從緩衝區中取出一個或多個數據項時再喚醒它。同樣地,當消費者試圖從緩衝區取資料而發現緩衝區為空時,消費者就睡眠,直到生產者向其中放入一些資料時再將其喚醒。

        這個方法聽起來很簡單,為了跟蹤緩衝區中的資料項數,我們需要一個變數count。如果緩衝區最多存放N個數據項,則生產者程式碼將首先檢查count是否達到N,若是,則生產者睡眠,否則生產者向緩衝區最多存放N個數據項,則生產者程式碼將首先檢查count是否達到N,若是,則生產者睡眠;否則生產者向緩衝區放入一個數據項並增量count的值。

        消費者的程式碼與此類似:首先測試count是否為0,若是,則睡眠;否則從中取出一個數據項並遞減count的值。每個程序同時也檢測另一個程序是否應該被喚醒,若是則喚醒之。生產者消費者的程式碼如下:

#define N 100
int count = 0;
void producer(void)
{
	int item;
	while(TRUE)
	{
		item = produce_item();
		if(count == N)					//如果緩衝區滿就休眠
		sleep();
		insert_item(item);
		count = count + 1;				//緩衝區資料項計數加1
		if(count == 1)
		wakeup(consumer);
	}
}

void consumer(void)
{
	int item;
	while(TRUE)
	{
		if(count == 0)				//如果緩衝區空就休眠
			sleep();
		item = remove_item();
		count = count - 1;			//緩衝區資料項計數減1
		if(count == N - 1)
			wakeup(producer);
		consume_item(item);
	}
}
        這裡有可能出現競爭條件,其原因是對count的訪問未作限制。有可能出現以下情況:緩衝區為空,消費者剛剛讀取count的值發現它為0,此時排程程式決定暫停消費者並啟動執行生產者。生產者向緩衝區加入一個數據項,count加1。現在count的值變成了1,它推斷認為count剛才為0,所以消費者此時一定在睡眠,於是生產者呼叫wakeup來喚醒消費者。

        但是消費者在邏輯上並未睡眠,所以wakeup訊號丟失,當消費者下次執行時,它將測試先前讀取的count值,發現它為0。於是睡眠,生產者遲早會填滿整個緩衝區,然後睡眠,這樣一來,兩個程序將永遠睡眠下去。

訊號量的引入及其操作

        訊號量是Dijkstra在1965年提出的一種方法,它使用一個整型變數來累計喚醒次數,供以後使用。在他的建議中引入了一個新的變數型別,稱作訊號量(semaphore)。一個訊號量的取值可以為0(表示沒有儲存下來的喚醒操作)或者正值(表示有一個或多個喚醒操作)。

        Dijkstra建議設立兩種操作:down和up(分別為一般化後的sleep和wakeup)。對一訊號量執行down操作,則是檢查其值是否大於0。若該值大於0,則將其減1(即用掉一個儲存的喚醒訊號)並繼續;若該值為0,則程序將睡眠,而且此時down操作並未結束。檢查數值、修改變數值以及可能發生的睡眠操作均作為一個單一的、不可分割的原子操作完成。保證一旦一個訊號量操作開始,則在該操作完成或阻塞之前,其他程序均不允許訪問該訊號量。這種原子性對於解決同步問題和避免競爭條件是絕對必要的。所謂原子操作,是指一組相關聯的操作要麼都不間斷地執行,要麼不執行

        up操作對訊號量的值增1。如果一個或多個程序在該訊號量上睡眠,無法完成一個先前的down操作,則由系統選擇其中的一個(如隨機挑選)並允許該程序完成它的down操作。於是,對一個有程序在其上睡眠的訊號量執行一次up操作後,該訊號量的值仍舊是0,但在其上睡眠的程序卻少了一個。訊號量的值增加1和喚醒一個程序同樣也是不可分割的,不會有某個程序因執行up而阻塞,正如前面的模型中不會有程序因執行wakeup而阻塞一樣。

        在Dijkstra原來的論文中,他分別使用名稱P和V而不是down和up,荷蘭語中,Proberen的意思是嘗試,Verhogen的含義是增加或升高。

        從物理上說明訊號量的P、V操作的含義。 P(S)表示申請一個資源,S.value>0表示有資源可用,其值為資源的數目;S.value=0表示無資源可用;S.value<0, 則|S.value|表示S等待佇列中的程序個數。V(S)表示釋放一個資源,訊號量的初值應該大於等於0。P操作相當於“等待一個訊號”,而V操作相當於“傳送一個訊號”,在實現同步過程中,V操作相當於傳送一個訊號說合作者已經完成了某項任務,在實現互斥過程中,V操作相當於傳送一個訊號說臨界資源可用了。實際上,在實現互斥時,P、V操作相當於申請資源和釋放資源

        該解決方案使用了三個訊號量:一個稱為full,用來記錄充滿緩衝槽數目,一個稱為empty,記錄空的緩衝槽總數;一個稱為mutex,用來確保生產者和消費者不會同時訪問緩衝區。full的初值為0,empty的初值為緩衝區中槽的數目,mutex的初值為1。供兩個或多個程序使用的訊號量,其初值為1,保證同時只有一個程序可以進入臨界區,稱作二元訊號量。如果每個程序在進入臨界區前都執行down操作,並在剛剛退出時執行一個up操作,就能夠實現互斥。

        在下面的例子中,我們實際上是通過兩種不同的方式來使用訊號量,兩者之間的區別是很重要的,訊號量mutex用於互斥,它用於保證任一時刻只有一個程序讀寫緩衝區和相關的變數。互斥是避免混亂所必需的操作。

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer(void)
{
	int item;
	while(TRUE)
	{
		item = produce_item();
		down(&empty);				//空槽數目減1,相當於P(empty)
		down(&mutex);				//進入臨界區,相當於P(mutex)
		insert_item(item);			//將新資料放到緩衝區中
		up(&mutex);				//離開臨界區,相當於V(mutex)
		up(&full);				//滿槽數目加1,相當於V(full)
	}
}
void consumer(void)
{
	int item;
	while(TRUE)
	{
		down(&full);				//將滿槽數目減1,相當於P(full)
		down(&mutex);				//進入臨界區,相當於P(mutex)
		item = remove_item();	   		 //從緩衝區中取出資料
		up(&mutex);				//離開臨界區,相當於V(mutex)		
		up(&empty);				//將空槽數目加1 ,相當於V(empty)
		consume_item(item);			//處理取出的資料項
	}
}

        訊號量的另一種用途是用於實現同步,訊號量full和empty用來保證某種事件的順序發生或不發生。在本例中,它們保證當緩衝區滿的時候生產者停止執行,以及當緩衝區空的時候消費者停止執行。

        對於無界緩衝區的生產者—消費者問題,兩個程序共享一個不限大小的公共緩衝區。由於是無界緩衝區(倉庫是無界限制的),即生產者不用關心倉庫是否滿,只管往裡面生產東西,但是消費者還是要關心倉庫是否空。所以生產者不會因得不到緩衝區而被阻塞,不需要對空緩衝區進行管理,可以去掉在有界緩衝區中用來管理空緩衝區的訊號量及其PV操作。

Semaphore mutex = 1; 
Semaphore full = 0; 
int in =0,out = 0;
void producer(void)
{
	while(TRUE)
	{
		item = produce_item();
		P(mutex);				//進入臨界區
		Buffer(in) = item;			//新生產的資料項放入緩衝區
		in = in + 1;				//因無界,無需考慮輸入指標越界
		V(mutex);				//離開臨界區
		V(full);				//增加已用緩衝區的數目
	}
}
void consumer(void)
{
	int item;
	while(TRUE)
	{
		P(full);			//等待已用緩衝區的數目非0
		P(mutex);			//進入臨界區
		item = Buffer(out);		//新生產的資料項放入緩衝區
		out = out + 1;			//因無界,無需考慮輸出指標越界
		V(mutex);			//離開臨界區
		consume_item(item);		//處理取出的資料項
	}
}

        在計算機領域,同步就是指一個程序在執行某個請求的時候,若該請求需要一段時間才能返回資訊,那麼這個程序會一直等待下去。直到收到返回資訊才繼續執行下去。非同步是指程序不需要一直等待下去,而是繼續執行下面的操作,不管其他程序的狀態,當有訊息返回時,系統會通知程序進行處理,這樣可以提高效率。