1. 程式人生 > >哲學家就餐問題的分析與解決方案

哲學家就餐問題的分析與解決方案

1.程序互斥與同步,死鎖基本知識

在多道程式環境下,程序有非同步和同步兩種併發執行方式。非同步執行是指執行中的各程序在作業系統的排程下以不可預知的速度向前推進。非同步執行的程序大多沒有時序要求,不存在“執行結果與語句的特定執行順序有關”的條件競爭。然而存在一類協作程序,“保證資料的一致性” 的前提要求它們必須按某種特定順序執行,並且遵守如下兩種限制。
(1)R1(順序化執行):程序A 的eventA事件必須發生在程序B的eventB事件之前;
(2)R2(互斥執行):程序 A的eventA事件與程序B的eventB事件不能同時發生。把上述限制下多程序的執行狀態叫作程序的同步執行。程序同步執行時因存在著明顯的執行上的時序要求而相互等待。如果說程序非同步是程序併發執行的自然結果,那麼程序同步則需要程式設計師通過準確嵌入一些諸如加解鎖來確保實現。
訊號量無疑是一個較為理想的同步工具。它最早由荷蘭科學家EdsgerDijkstra於1965年提出,該工具具有如下三個優點:(1)僅需要兩個基本操作即可完成程序的同步和互斥,而且兩個原子操作程式碼簡潔高效, 易於擴充;(2) 精心設計的訊號量物件類似一條條“觸發器”規則,加上訊號量機制的強制作用可以幫助程式設計師少犯錯誤;(3)訊號量已在很多系統中實現,解決方案中有意識地選用訊號量無疑將使程序更“瘦身”,執行更高效。訊號量技術的引入是對早期忙等型(busywaiting)程序控制變數是個巨大的提升,但在使用過程中仍然存在不少缺點:一是不能隨時讀取訊號量的值, 必要時須重複定義一個跟蹤訊號量值的普通變數,二是程式設計師對訊號量的PV操作的正確使用與否沒有任何控制和保證(後來引入管程和條件變數,PV操作完全由編譯器而非 程式設計師安排),不合理地使用將導致程序飢餓甚至死鎖。死鎖應儘可能阻止,系統死鎖導致諸程序將進入無法向前推進的僵持狀態, 除非藉助於外力。死鎖的原因除了系統資源偏少之外,更多的是程序推進速度不當, 或者說程序申請和釋放訊號量的順序不合理所致,畢竟系統提供的資源是有限的。以哲學家就餐問題為例,若派發給每位哲學家一雙筷子(更準確地說,6支就足夠), 則一定不會死鎖。事實上,若訊號量的PV操作順序處置得當,5支筷子同樣也可以保證不會發生死鎖。
死鎖是 《作業系統原理》課程中的1個很重要的概念, 它描述的是多個程序因競爭資源而造成的1種僵局 ,若無外力作用 ,這些程序將永遠不能再向前推進。產生死鎖的原因主要有2點: 1是競爭資源 ; 2是程序推進順序不當。

2.哲學家就餐問題

哲學家就餐問題是在電腦科學中的一個經典問題,用來演示在平行計算中多執行緒同步(Synchronization)時產生的問題。在1971年,著名的電腦科學家艾茲格•迪科斯徹提出了一個同步問題,即假設有五臺計算機都試圖訪問五份共享的磁帶驅動器。稍後,這個問題被託尼•霍爾重新表述為哲學家就餐問題。這個問題可以用來解釋死鎖和資源耗盡。
哲學家就餐問題可以這樣表述,假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:吃飯,或者思考。吃東西的時候,他們就停止思考,思考的時候也停止吃東西。餐桌中間有一大碗義大利麵,每兩個哲學家之間有一隻餐叉。因為用一隻餐叉很難吃到義大利麵,所以假設哲學家必須用兩隻餐叉吃東西。他們只能使用自己左右手邊的那兩隻餐叉。哲學家就餐問題有時也用米飯和筷子而不是義大利麵和餐叉來描述,因為很明顯,吃米飯必須用兩根筷子。
哲學家從來不交談,這就很危險,可能產生死鎖,每個哲學家都拿著左手的餐叉,永遠都在等右邊的餐叉(或者相反)。即使沒有死鎖,也有可能發生資源耗盡。例如,假設規定當哲學家等待另一隻餐叉超過五分鐘後就放下自己手裡的那一隻餐叉,並且再等五分鐘後進行下一次嘗試。這個策略消除了死鎖(系統總會進入到下一個狀態),但仍然有可能發生“活鎖”。如果五位哲學家在完全相同的時刻進入餐廳,並同時拿起左邊的餐叉,那麼這些哲學家就會等待五分鐘,同時放下手中的餐叉,再等五分鐘,又同時拿起這些餐叉。
在實際的計算機問題中,缺乏餐叉可以類比為缺乏共享資源。一種常用的計算機技術是資源加鎖,用來保證在某個時刻,資源只能被一個程式或一段程式碼訪問。當一個程式想要使用的資源已經被另一個程式鎖定,它就等待資源解鎖。當多個程式涉及到加鎖的資源時,在某些情況下就有可能發生死鎖。例如,某個程式需要訪問兩個檔案,當兩個這樣的程式各鎖了一個檔案,那它們都在等待對方解鎖另一個檔案,而這永遠不會發生。

3. 訊號量機制解決哲學家就餐問題

當5個哲學家程序併發執行時,某個時刻恰好每個哲學家程序都執行申請筷子,並且成功申請到第i支筷子(相當於5個哲學家同時拿起他左邊的筷子), 接著他們又都執行申請右邊筷子, 申請第i+1支筷子。此時每個哲學家僅拿到一支筷子, 另外一支只得無限等待下去, 引起死鎖。在給出幾種有效阻止死鎖的方案之前,首先給出兩個斷言:
(1)系統中有N個併發程序。 若規定每個程序需要申請2個某類資源, 則當系統提供N+1個同類資源時,無論採用何種方式申請資源, 一定不會發生死鎖。分析:N+1個資源被N 個程序競爭, 由抽屜原理可知, 則至少存在一個程序獲2個以上的同類資源。這就是前面提到的哲學家就餐問題中5個哲學家提供6支筷子時一定不會發生死鎖的原因。
(2)系統中有N個併發程序。 若規定每個程序需要申請R個某類資源, 則當系統提供K=N*(R-1)+1個同類資源時,無論採用何種方式申請使用,一定不會發生死鎖。分析:在最壞的情況下,每個程序都申請到R-1個同類資源, 此時它們均阻塞。 試想若系統再追加一個同類資源, 則 N 個程序中必有一個程序獲得R個資源,死鎖解除。
結合以上分析,哲學家就餐問題可以被抽象描述為:系統中有5個併發程序, 規定每個程序需要申請2個某類資源。 若系統提供5個該類資源, 在保證一定不會產生死鎖的前提下,最多允許多少個程序併發執行?假設允許N個程序, 將R=2,K=5帶入上述公式, 有N*(2-1)+1=5所以 N=4。也就意味著,如果在任何時刻系統最多允許4個程序併發執行, 則一定不會發生死鎖。 大多數哲學家就餐問題死鎖阻止演算法都是基於這個結論。
增加一個訊號量,控制最多有4個程序併發執行,演算法如下:

Semaphorechopstick[5]={1,1,1,1,1};//分別表示5支筷子
Semaphorefootman=4;//初始值為4最多允許4個哲學家程序同時進行
Philosopher(inti)
{while(true)
    {wait(footman);
    Think();
    Wait(chopstick[i]);//申請左筷子
    Wait(chopstick[(i+1)%5]);//申請右筷子
    Eat();
    Signal(chopstick[i]);//釋放左筷子
    Signal(chopstick[(i+1)%5]);//釋放右筷子
    Signal(footman);
    }
}

4.基於退回機制的哲學家進餐問題的解決

回退機制的理論依據是處理死鎖基本方法中的預防死鎖策略。預防死鎖是指通過設定某些限制條件 ,去破壞產生死鎖的 4 個必要條件中的一個或幾個來防止死鎖的發生. 其中“摒棄不剝奪條件”規定,當一個程序提出新的資源請求而不能立即得到滿足時 ,必須釋放其已經保持了的所有資源 , 即已經佔有的資源可以被剝奪。根據上面的理論 ,本文解決哲學家進餐問題的基本思路是 ,讓每名哲學家先去申請他左手邊的筷子 ,然後再申請他右手邊的筷子 ,如果右手邊的筷子不空閒, 則比較當前哲學家 i 和他右手邊的哲學家( i +1)%5 ,看誰取得各自左手邊筷子的時間更晚, 令其回退( 即釋放左手筷子, 再重新申請左手筷子), 直到此哲學家右手邊的筷子空閒為止。通過設定回退機制可以確保每位哲學家都能順利進餐。
通過訊號量來描述該演算法 ,程式碼如下:

semaphore chopstick[ 04] ={ 1 , 1 , 1 , 1 , 1};
/*chopstick [ ] :筷子訊號量陣列,初始值均為 1 ,表示開始時 5 根筷子都可用*/
philosopher ( i) // i : 哲學家編號, 從0到4
{ think( );//哲學家正在思考
wait( chopstick( i) );//取左側的筷子
while (右手邊筷子不空閒)
    { if (當前哲學家i比旁邊哲學家( i +1)%5 晚拿到左手筷子) //%為取模運算
       {哲學家i釋放左手邊的筷子;
       think( );//哲學家i思考
       哲學家i重新取左側的筷子;
       } 
else
{哲學家( i +1)%5 釋放左手邊的筷子;
think( );//哲學家( i +1)%5 思考
哲學家( i +1)%5 重新取左側的筷子;
 }
}
wait( chopstick( ( i +1)%5) );//取右側筷子
eat();//進餐
signal( chopstick( i) );//把左側筷子放回原位
signal( chopstick( ( i +1)%5) );//把右側筷子放回原位
}

5.用附加規則解決哲學家進餐問題

為了預防死鎖的產生,我們新增一條競爭規則:所有哲學家先競爭奇數號筷子,獲得後才能去競爭偶數號筷子(由於5號哲學家左右都是奇數號筷子,在本文中規定他先競爭5號筷子)。這樣的話就總會有一名哲學家可以順利獲得兩支筷子開始進餐。此方法的本質是通過附加的規則,讓哲學家按照一定的順序請求臨界資源——筷子。這樣的話,在資源分配圖中就不會出現環路,破壞了死鎖生的必要條件之一:“環路等待”條件,從而有效地預防了死鎖的產生。接下來我們用 Java 語言來實現該剛才描述的策略。在實現程式碼中用五個執行緒表示五個哲學家的活動, 用一個邏輯型陣列表示筷子的狀態。 在此問題中,筷子是臨界資源,必須互斥地進行訪問。我們為筷子定義一個類,其中包含了表示筷子狀態的邏輯。

class Chopsticks
{
/* 用 used[1]至 used[5]五個陣列元素分別代表編號 1 至 5 的五支筷
子的狀態 */
/* false 表示未被佔用,true 表示已經被佔用。 used[0]元素在程式中
未使用 */
private boolean used[]={true,false,false,false,false,false};
/* 拿起筷子的操作 */
public synchronized void takeChopstick()
{
/* 取得該執行緒的名稱並轉化為整型,用此整數來判斷該哲學家應該用哪兩支筷子 */
/* i 為左手邊筷子編號,j 為右手邊筷子編號 */
String name=Thread.currentThread().getName();
int i=Integer.parseInt(name);
/* 1~4 號哲學家使用的筷子編號是 i 和 i+1,5 號哲學家使用
的筷子編號是 5 和 1 */
int j=i==5?1:i+1;
/* 將兩邊筷子的編號按奇偶順序賦給 odd,even 兩個變數 */
int odd,even;
if(i%2==0){even=i;odd=j;}
else {odd=i;even=j;}
/* 首先競爭奇數號筷子 */
while(used[odd])
{
try{wait();}
catch(InterruptedException e){}
}
used[odd]=true;
/* 然後競爭偶數號筷子 */
while(used[even])
{
try{wait();}
catch(InterruptedException e){}
}
used[even]=true;
}/*放下筷子的操作 */
public synchronized void putChopstick()
{
String name=Thread.currentThread().getName();
int i=Integer.parseInt(name);
int j=i==5?1:i+1;
/* 將相應筷子的標誌置為 fasle 表示使用完畢, 並且通知其
他等待執行緒來競爭 */
used[i]=false;
used[j]=false;
notifyAll();
}
}

當某一哲學家執行緒執行取得筷子方法時, 程式會根據該執行緒的名稱來確定該執行緒需要使用哪兩支筷子,並且分辨出哪支筷子編號是奇數,按照先奇後偶的順序來試圖取得這兩支筷子。 如果這兩支筷子都未被使用(即對應的陣列元素值為 false),該哲學家執行緒即可先後取得這兩支筷子進餐,否則會在競爭某支筷子失
敗後執行 wait()操作進入 Chopsticks 類例項的等待區, 直到其他的哲學家執行緒進餐完畢放下筷子時用 notifyAll()將其喚醒。當某一哲學家執行緒放下筷子時, 程式會將放下的筷子對應的陣列元素值置為 false,並用 notifyAll()喚醒在等待區裡的其他執行緒。
接下來定義出哲學家類

class Philosopher extends Thread
{
Chopsticks chopsticks;
public Philosopher(String name,Chopsticks chopsticks)
{
/* 在構造例項時將 name 引數傳給 Thread 的建構函式作為執行緒的名稱 */
super(name);
/* 所有哲學家執行緒共享同一個筷子類的例項 */this.chopsticks=chopsticks;
}
public void run()
{
/* 交替地思考、拿起筷子、進餐、放下筷子 */
while(true)
{
thinking();
chopsticks.takeChopstick();
eating();
chopsticks.putChopstick();
}
}
public void thinking()
{
/* 顯示字串輸出正在思考的哲學家,用執行緒休眠1秒鐘來模擬思考時間 */
System.out.println ("Philosopher " +Thread.currentThread ().getName()+" is thinking.");
try{Thread.sleep(1000);}
catch(InterruptedException e){}
}
public void eating()
{
/* 顯示字串輸出正在進餐的哲學家,並用執行緒休眠 1 秒鐘來模擬進餐時間 */
System.out.println ("Philosopher " +Thread.currentThread ().getName()+" is eating.");
try{Thread.sleep(1000);}
catch(InterruptedException e){}
}
} 

在執行時,用Philosopher 類產生五個執行緒模擬五個哲學家,每個執行緒不停地重複執行思考、拿起筷子、進餐、放下筷子的過程。 執行緒的名稱依次為“1”,
“2”,“3”,“4”,“5”(字串型別)
主程式如下

public class Mainz
{
public static void main(String[] args)
{
/* 產生筷子類的例項 chopsticks */
Chopsticks chopsticks=new Chopsticks();
/* 用筷子類的例項作為引數, 產生五個哲學家執行緒並啟動*/
/* 五個哲學家執行緒的名稱為 1~5 */
new Philosopher("1",chopsticks).start();
new Philosopher("2",chopsticks).start();
new Philosopher("3",chopsticks).start();
new Philosopher("4",chopsticks).start();
new Philosopher("5",chopsticks).start();
}
}
執行後,從輸出的結果可以看到五個哲學家執行緒交替地進行思考和進餐,互斥地使用筷子,有效地避免了死鎖的發生。

6.總結

本文對哲學家進餐問題產生死鎖的現象進行了分析,提出了3種解決方案, 並從在理論依據、演算法設計、程式設計實現等方面進行了較為詳細地闡述。這對學習和理解《作業系統原理》課程中的經典程序同步問題有一定的參考價值。