生產者-消費者之經典進程的同步問題

生產者-消費者問題(英語:Producer-consumer problem),也稱有限緩衝問題(英語:Bounded-buffer problem),是一個多線程同步問題 的經典案例。該問題描述了共享固定大小緩衝區的兩個線程——即所謂的“生產者”和“消費者”——在實際運行時會發生的問題。
即有一羣生產者進程在生成產品,並將這些產品提供給消費者進程去消費。
爲使生產者進程與消費者進程能併發執行,在兩者之間設置了一個具有n個緩衝池,生產者將其所生產的產品放入一個緩衝區中;消費者進程可從一個緩衝區中取走產品去消費。儘管所有的生產者進程和消費者進程都是以異步方式 運行,但它們之間必須保持同步,既不運行消費者進程到一個空緩衝區去取產品,也不允許生產者進程向一個已裝滿產品且尚未被取走的緩衝區中投放產品。

生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據。

可利用一個數組buffer來表示上述的具有n個緩衝區的緩衝池。每投入(或取出)一個產品時,緩衝池buffer中暫存產品(或已取走產品的空閒單元)的數組單元指針in(或out) 加1.
由於這裏由buffer組成的緩衝池是被組織成循環緩衝的,故應把輸入指針in(或輸出指針out)加1,表示成in=(in+1)%n(或out=(out+1)%n)
當(in+1)%n=out時表示緩衝池滿;
而in=out則表示緩衝池空。
此外,還引入了一個整形變量counter,其初始值爲0。每當生產者進程向緩衝池中投放(或取走)一個產品後,使counter加1(或減1)。
生產者和消費者兩進程共享下面的變量:
int in=0,out=0,count=0;
item buffer[n];

指針in和out初始化爲0.在生產者進程中使用一局部變量nextp,用於暫時存放每次剛剛生產出來的產品;而在消費者進程中,則使用一個局部變量nextc,用於存放每次要消費的產品。
兩者分別看或在順序執行時其結果都是正確的,但若併發執行時就會出現差錯,問題就在於兩個進程共享變量counter,生產者對其做加1操作,消費者對其做減1操作。
分別執行後共享變量counter的值不變,或致使程序執行後失去再現性,即未考慮進程的互斥與同步問題,造成數據counter的不定性。
爲了預防產生這種錯誤,解決此問題的關鍵是應把變量counter作爲臨界資源處理,亦即,令生產者進程和消費者進程互斥地訪問變量counter。

由此可知,不論是硬件臨界資源還是軟件臨界資源,多個進程必須互斥地對它進行訪問。人們把在每個進程中訪問臨界資源的那段代碼稱爲臨界區(critical section)。
顯然,若能保證諸進程互斥地進入自己的臨界區,便可實現諸進程的臨界資源的互斥訪問。爲此,每個進程在進入臨界區之前,應先對欲訪問的臨界資源進行檢查,看它是否正在被訪問。
如果此刻臨界資源未被訪問,進程便可進入臨界區對資源進行訪問,並設置它正被訪問的標誌;如果此刻該臨界資源正被某進程訪問,則本進程不能進入臨界區。
由此,必須在臨界區前面增加一段用於進行上述檢查的代碼,把這段代碼稱爲進入區(enter section)。相應地,在臨界區後面也要加上一段稱爲退出區(exit section)的代碼,用於將臨界區正被訪問的標誌恢復爲未被訪問的標誌。進程區除上述的進入區、臨界區及退出區之外的其他部分的代碼都稱爲剩餘區

爲實現進程互斥地進入自己的臨界區,可採用軟件方法,更多的是在系統中設置專門的同步機制來協調各進程間的運行。所有同步機制都應遵循下述四條準則

  • 空閒讓進:當無進程處於自己的臨界區時,表明臨界資源處於空閒狀態,應允許一個請求進入臨界區的進程立即進入自己的臨界區,以有效地利用臨界資源。
  • 忙則等待:當已有進程進入臨界區,表明臨界資源正在被訪問,因而其它試圖進入臨界區的進程必須等待,以保證對臨界資源的互斥訪問。
  • 有限等待:對要求訪問臨界資源的進程,應保證在有限時間內能進入自己的臨界區,以免陷入“死等”狀態。
  • 讓權等待:當進程不能進入自己的臨界區時,應立即釋放處理機,以免進程陷入“忙等”狀態。

綜上可知,要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆就放棄數據),等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒,開始往緩衝區添加數據。同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區添加數據之後,再喚醒消費者。通常採用進程間通信的方法解決該問題。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。該問題也能被推廣到多個生產者和消費者的情形。

由於生產者和消費者問題是相互合作的進程關係的一種抽象,例如,在輸入時,輸入進程是生產者,計算進程是消費者;而在輸出進程時,則計算進程是生產者,而打印進程是消費者,因此,該問題有很大的代表性及實用價值。
可利用信號量機制來解決該問題。
利用記錄型信號量解決生產者-消費者問題:
假設在生產者和消費者之間的公用緩衝池中具有n個緩衝區,這時可利用互斥信號量mutex實現諸進程對緩衝池的互斥作用;利用信號量empty和full分別表示緩衝池中空緩衝區和滿緩衝區的數量。又假定這些生產者和消費者互相等效,只要緩衝池未滿,生產者便可將消息送入緩衝池;只要緩衝池未空,消費者便可從緩衝池中取走一個消息。需要注意的是:
首先,在每個程序中用於實現互斥的wait(mutex)和signal(mutex)必須成對出現;其次,對資源信號量empty和full的wait和signal操作,同樣需要成對出現,但它們分別處於不同的出現中。
例如,wait(empty)在計算進程中,而signal(empty)則在打印進程中,計算進程若因執行wait(empty)而阻塞,則以後將由打印進程將它喚醒;最後,在每個程序中的多個wait操作順序不能顛倒。應先執行對資源信號量的wait操作,然後再執行對互斥信號量的wait操作,否則可能引起進程死鎖。
利用AND信號量解決生產者-消費者問題:
對於生產者-消費者問題,也可用ADN信號量來解決,即用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)。
利用管程解決生產者-消費者問題:
在利用管程方法解決生產者-消費者問題時,首先便是爲它們建立一個管程,並命名爲producerconsumer,或簡稱爲PC。其中包括兩個過程:
put(x)過程:生產者利用該過程將自己生產的產品投放到緩衝池中,並用整形變量count來表示在緩衝池中已有的產品數目,當count>=N時,表示緩衝池已滿,生產者需等待。
get(x)過程:消費者利用該過程將緩衝池中取出一個產品,當count<=0時,表示緩衝池已無可取用的產品,消費者應等待。
對於變量notfull和notempty,分別有兩個過程cwait和csignal對它們進行操作:
cwait(condition)過程:當管程被一個進程佔用時,其他進程調用該進程是阻塞,並掛在條件condition的隊列中。
csignal(condition)過程:喚醒在cwait執行後阻塞在條件condition隊列上的進程,如果這樣的進程不止一個,則選擇其中一個實施喚醒操作;如果隊列爲空,則無操作返回。

經典進程的同步問題不僅有“生產者-消費者”問題,類似的還有“讀者-寫者問題”、“哲學家進餐問題”等等。

讀者-寫者問題

一個數據文件或記錄可被多個進程共享。

  • 只要求讀文件的進程稱爲“Reader進程”,其它進程則稱爲“Writer進程”。
  • 允許多個進程同時讀一個共享對象,但不允許一個Writer進程和其他Reader進程或Writer進程同時訪問共享對象

“讀者—寫者問題”是保證一個Writer進程必須與其他進程互斥地訪問共享對象的同步問題。
可使用記錄型信號量或信號量集機制解決讀者-寫者問題
1.利用記錄型信號量解決讀者-寫者問題
爲實現Reader與Writer進程間在讀或寫時的互斥而設置了一個互斥信號量wmutex。
另外,再設置一個整數變量readcount表示正在讀的進程數目。
由於只要有一個Reader進程在讀,便不允許Writer進程寫,因此僅當Readcount=0,即無Reader進程在讀時,
Reader才需要執行Wait(wmutex)操作。若Wait(wmutex)操作成功,Reader進程便可去讀,相應地,做Readcount+1操作。
同理,僅當Reader進程在執行了Readcount減1操作後其值爲0時,才需執行signal(wmutex)操作,以便讓Write進程寫。
由於Readcount爲多個讀進程共享(修改),因此需要以互斥方式訪問,爲此,需要定義互斥信號量rmutex,保證讀進程間互斥訪問Readcount。

2.利用信號量集機制解決讀者-寫者問題
增加一個限制:最多隻允許RN個讀者同時讀。
引入信號量L,並賦予其初值RN,通過執行Swait(L, 1, 1)操作,來控制讀者的數目。
每當有一個讀者進入時,就要先執行Swait(L, 1, 1)操作,使L的值減1。
當有RN個讀者進入讀後,L便減爲0,第RN +1個讀者要進入讀時,必然會因Swait(L, 1, 1)操作失敗而阻塞。

哲學家進餐問題

由荷蘭學者Dijkstra提出的哲學家進餐問題(The Dinning Philosophers Problem)是經典的同步問題之一。該問題描述有五個哲學家,他們的生活方式是交替地進行思考和進餐,哲學家們共用一張圓桌,分別坐在周圍的五張椅子上,在圓桌上有五個碗和五支筷子,平時哲學家進行思考,飢餓時便試圖取其左、右最靠近他的筷子,只有在他拿到兩支筷子時才能進餐,該哲學家進餐完畢後,放下左右兩隻筷子又繼續思考。
可使用記錄型信號量或AND信號量機制解決哲學家進餐問題
1.利用記錄型信號量解決哲學家進餐問題
經分析可知,放在桌子上的筷子是臨界資源,在一段時間內只允許一位哲學家使用。爲了實現筷子的互斥使用,可以使用一個信號量表示一隻筷子,所有信號量被初始化爲1,由這五個信號量構成信號量數組。
創建五個不同的線程代表五位不同的哲學家。每位哲學家先思考,當某位哲學家飢餓的時候,就拿起他左邊的筷子,然後拿起他右邊的筷子,然後進餐,然後放下他左右的筷子並進行思考。因爲筷子是臨界資源,所以當一位哲學家拿起他左右的筷子的時候,就會對他左右的筷子進行加鎖,使其他的哲學家不能使用,當該哲學家進餐完畢後,放下了筷子,纔對資源解鎖,從而使其他的哲學家可以使用。
​這個過程看似沒什麼問題,但是當你仔細分析之後,你會發現這裏面有一個很嚴重的問題,就是死鎖,就是每個線程都等待其他線程釋放資源從而被喚醒,從而每個線程陷入了無限等待的狀態。在哲學家就餐問題中,一種出現死鎖的情況就是,假設一開始每位哲學家都拿起其左邊的筷子,然後每位哲學家又都嘗試去拿起其右邊的筷子,這個時候由於每根筷子都已經被佔用,因此每位哲學家都不能拿起其右邊的筷子,只能等待筷子被其他哲學家釋放。由此五個線程都等待被其他進程喚醒,因此就陷入了死鎖。
解決死鎖問題的幾種方法:

  • (1)至多隻允許四位哲學家同時拿起同一邊的筷子,這樣就能保證一定會有一位哲學家能夠拿起兩根筷子完成進食並釋放資源,供其他哲學家使用,從而實現永動,避免了死鎖。
  • (2)僅當哲學家的左、右兩隻筷子可用時,才允許他拿起筷子進餐。
  • (3)規定奇數號哲學家先拿起他左邊的筷子,然後再去拿右邊的筷子;而偶數號哲學家則相反。

2.利用AND信號量機制解決哲學家進餐問題
如果想給某個哲學家筷子,就將他需要的所有資源都給他,然後讓他進餐,否則就一個都不給他。本質就是前面介紹的AND同步問題。

哲學家進餐問題是一大類併發控制問題的典型例子,涉及信號量機制、管程機制以及死鎖等操作系統中關鍵問題的應用,在操作系統文化史上具有非常重要的地位。
對該問題的剖析有助於深刻地理解計算機系統中的資源共享、進程同步機制、死鎖等問題,並能熟練地將該問題的解決思想應用於生活中的控制流程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章