1. 程式人生 > >多執行緒基礎——經典的哲學家就餐問題

多執行緒基礎——經典的哲學家就餐問題

哲學家就餐問題是在電腦科學 中的一個經典問題,用來演示在平行計算 中多執行緒同步時產生的問題。這個問題被託尼·霍爾重新表述為哲學家就餐問題。這個問題可以用來解釋死鎖和資源耗盡。用來演示在平行計算中多執行緒同步時產生的問題, 就可以抽象成是資源搶佔問題,而筷子就是“資源”。
哲學家就餐問題可以這樣表述:假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:吃飯,或者思考。吃東西的時候就停止思考,思考的時候也停止吃東西。餐桌中間有一大碗義大利麵,每兩個哲學家之間有一根筷子。因為用一根筷子很難吃到義大利麵,所以哲學家必須用兩根筷子吃東西,而且他們也只能使用自己左右手邊的那兩隻筷子。 哲學家從來不交談,這就很危險,可能產生死鎖,每個哲學家都拿著左手的筷子,永遠都在等右邊的筷子(或者相反)。即使沒有死鎖,也有可能發生資源耗盡。例如,假設規定當哲學家等待另一隻筷子超過五分鐘後就放下自己手裡的那一隻筷子,並且再等五分鐘後進行下一次嘗試。這個策略消除了死鎖(系統總會進入到下一個狀態),但仍然有可能發生“活鎖”。如果五位哲學家在完全相同的時刻進入餐廳,並同時拿起左邊的筷子,那麼這些哲學家就會等待五分鐘,同時放下手中的筷子,再等五分鐘,又同時拿起這些筷子。
先介紹一下死鎖、活鎖: 死鎖:
是指兩個或兩個以上的程序(或執行緒)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。規範定義:集合中的每一個程序都在等待只能由本集合中的其他程序才能引發的事件,那麼該組程序是死鎖的。 死鎖發生的四個條件: 1、互斥條件:執行緒對資源的訪問是排他性的,如果一個執行緒對佔用了某資源,那麼其他執行緒必須處於等待狀態,直到資源被釋放。 2、請求和保持條件:執行緒T1至少已經保持了一個資源R1佔用,但又提出對另一個資源R2請求,而此時,資源R2被其他執行緒T2佔用,於是該執行緒T1也必須等待,但又對自己保持的資源R1不釋放。 3、不剝奪條件:執行緒已獲得的資源,在未使用完之前,不能被其他執行緒剝奪,只能在使用完以後由自己釋放。 4、環路等待條件:在死鎖發生時,必然存在一個“程序-資源環形鏈”,即:{p0,p1,p2,...pn},程序p0(或執行緒)等待p1佔用的資源,p1等待p2佔用的資源,pn等待p0佔用的資源。(最直觀的理解是,p0等待p1佔用的資源,而p1而在等待p0佔用的資源,於是兩個程序就相互等待) 產生死鎖的原因主要是: (1) 因為系統資源不足。 (2) 程序執行推進的順序不合適。 (3) 資源分配不當等。 如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,程序執行推進順序與速度不同,也可能產生死鎖。
活鎖:
是指執行緒1可以使用資源,但它很禮貌,讓其他執行緒先使用資源,執行緒2也可以使用資源,但它很紳士,也讓其他執行緒先使用資源。這樣你讓我,我讓你,最後兩個執行緒都無法使用資源。
關於“死鎖與活鎖”的比喻: 死鎖:迎面開來的汽車A和汽車B,汽車A得到了半條路的資源(死鎖發生條件1:資源訪問是排他性的,A佔了路你B不能過來,除非B從A頭上飛過去),汽車B佔了汽車A的另外半條路的資源,A想過去必須請求另一半被B佔用的道路(死鎖發生條件2:必須整條車身的空間才能開過去,A已經佔了一半,但是另一半的路被B佔用了),B若想過去也必須等待A讓路,但是因為一些原因兩方都沒有讓路(死鎖發生條件3:在未使用完資源前,不能被其他執行緒剝奪),於是兩者相互僵持一個都走不了(死鎖發生條件4:環路等待條件),而且導致整條道上的後續車輛也走不了。 活鎖:馬路中間有條小橋,只能容納一輛車經過,橋兩頭開來兩輛車A和B,A比較禮貌,示意B先過,B也比較禮貌,示意A先過,結果兩人一直謙讓誰也過不去。
在實際的計算機問題中,缺乏餐叉可以類比為缺乏共享資源。一種常用的計算機技術是資源加鎖,用來保證在某個時刻,資源只能被一個程式或一段程式碼訪問。當一個程式想要使用的資源已經被另一個程式鎖定,它就等待資源解鎖。當多個程式涉及到加鎖的資源時,在某些情況下就有可能發生死鎖。例如,某個程式需要訪問兩個檔案,當兩個這樣的程式各鎖了一個檔案,那它們都在等待對方解鎖另一個檔案,而這永遠不會發生。
問題解法

1.服務生解法 一個簡單的解法是引入一個餐廳服務生,哲學家必須經過他的允許才能拿起餐叉。因為服務生知道哪隻餐叉正在使用,所以他能夠作出判斷避免死鎖。 為了演示這種解法,假設哲學家依次標號為A至E。如果A和C在吃東西,則有四隻餐叉在使用中。B坐在A和C之間,所以兩隻餐叉都無法使用,而D和E之間有一隻空餘的餐叉。假設這時D想要吃東西。如果他拿起了第五隻餐叉,就有可能發生死鎖。相反,如果他徵求服務生同意,服務生會讓他等待。這樣,我們就能保證下次當兩把餐叉空餘出來時,一定有一位哲學家可以成功的得到一對餐叉,從而避免了死鎖。
2.資源分級解法 另一個簡單的解法是為資源(這裡是餐叉)分配一個偏序或者分級的關係,並約定所有資源都按照這種順序獲取,按相反順序釋放,而且保證不會有兩個無關資源同時被同一項工作所需要。在哲學家就餐問題中,資源(餐叉)按照某種規則編號為1至5,每一個工作單元(哲學家)總是先拿起左右兩邊編號較低的餐叉,再拿編號較高的。用完餐叉後,他總是先放下編號較高的餐叉,再放下編號較低的。在這種情況下,當四位哲學家同時拿起他們手邊編號較低的餐叉時,只有編號最高的餐叉留在桌上,從而第五位哲學家就不能使用任何一隻餐叉了。而且,只有一位哲學家能使用最高編號的餐叉,所以他能使用兩隻餐叉用餐。當他吃完後,他會先放下編號最高的餐叉,再放下編號較低的餐叉,從而讓另一位哲學家拿起後邊的這隻開始吃東西。 儘管資源分級能避免死鎖,但這種策略並不總是實用的,特別是當所需資源的列表並不是事先知道的時候。例如,假設一個工作單元拿著資源3和5,並決定需要資源2,則必須先要釋放5,之後釋放3,才能得到2,之後必須重新按順序獲取3和5。對需要訪問大量資料庫記錄的計算機程式來說,如果需要先釋放高編號的記錄才能訪問新的記錄,那麼執行效率就不會高,因此這種方法在這裡並不實用。 這種方法經常是實際電腦科學問題中最實用的解法,通過為分級鎖指定常量,強制獲得鎖的順序,就可以解決這個問題。
3.Chandy/Misra解法 1984年,K. Mani Chandy和J. Misra提出了哲學家就餐問題的另一個解法,允許任意的使用者(編號P1, ..., Pn)爭用任意數量的資源。與迪科斯徹的解法不同的是,這裡編號可以是任意。完全的分散式的方法,不需要仲裁者。 以下是一些情況下遇到的判斷: (1) 首先,給哲學家編號。 (2) 若兩個哲學家競爭同一個筷子的時候,把筷子給編號比較小的人。 (3) 筷子有兩個狀態,淨的和髒的。初始狀態下,所有的筷子都是髒的。 (4) 哲學家要吃飯時必須拿起兩隻筷子,當他缺乏某隻筷子的時候,發起請求。 (5) 若拿著筷子的哲學家受到了請求,如果筷子是乾淨的,則他不放下筷子;若是髒的,則放下筷子,並且在放下之前,把筷子擦乾淨。 (6) 哲學家吃完的時候,筷子變髒,當他有鄰座請求筷子的時候,擦乾淨筷子,然後給鄰座。 根據之上的判斷出現了以下的流程:
  1. 起初,先把 5 只筷子分別分給 A~E 五位哲學家,並定義為髒的。
  2. 假設B想要吃東西了,他把手裡的2號筷子擦乾淨,可是還缺1號筷子呀,於是就向擁有1號筷子的A傳送一個請求。
  3. A收到請求,因為此時1號筷子是髒的,所以A得把1號筷子擦乾淨,並交給B。
  4. B有兩隻筷子了,準備開吃了。可就在此時,C也想吃東西了,向B傳送過來一個請求,想要2號筷子。
  5. B手裡的2號筷子是乾淨的呀,於是B先吃自己的,吃完以後2號筷子變成髒的了,B再把筷子擦乾淨並交給C。 
.......... ..... 這個解法允許很大的並行性,適用於任意大的問題。 程式碼例項: package Philosopher.ChandyMisra; import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;class Stick { int owner; boolean dirty; int number; public synchronized void setStatus( boolean status){ dirty = status; } public synchronized boolean getStatus(){ return this .dirty; } public synchronized void setOwner( int owner){ this .owner = owner; } public synchronized int getOwner(){ return owner; } public Stick( boolean dirty, int number) { this .dirty = dirty; this .number = number; }} public class Philo implements Runnable { Stick leftStick, rightStick; Philo leftPhilo, rightPhilo; int number, eatTimes; public Philo( int number, int eatTimes) { this .number = number; this .eatTimes = eatTimes; } public void eating(){ try { Thread.sleep( 3000 ); } catch (InterruptedException ie){ System.out.println( "Catch an Interrupted Exception!" ); return ; } leftStick.setStatus( true ); rightStick.setStatus( true ); eatTimes++; System.out.println( "Philo " + number + "is eating " + ": " + eatTimes ); //dinningExam.repaint(); } public boolean answer(Stick used){ boolean retFlag = false ; synchronized ( this ){ if (used.getStatus()){ if (used == leftStick) used.setOwner(leftPhilo.number); else if (used == rightStick) used.setOwner(rightPhilo.number); else { System.out.println( "Error status!" ); retFlag = false ; } used.setStatus( false ); retFlag = true ; } else retFlag = false ; } if (retFlag) System.out.println( "Philo " + number + "request success!" ); //dinningExam.repaint(); return retFlag; } @Override public void run() { Random r = new Random(); int intR; while ( true ){ while (leftStick.getOwner() != number | rightStick.getOwner() != number){ intR = r.nextInt(); if (intR % 2 == 0 && leftStick.getOwner() != number) leftPhilo.answer(leftStick); else if (intR % 2 == 1 && rightStick.getOwner() != number) rightPhilo.answer(rightStick); } synchronized ( this ){ if (leftStick.getOwner() == number && rightStick.getOwner() == number){ eating(); } } try { int sleepSec = r.nextInt(); if (sleepSec < 0 ) sleepSec = -sleepSec; Thread.sleep(sleepSec % 500 ); } catch (InterruptedException ie){ System.out.println( "Catch an Interrupted Exception!" ); } } } public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); Philo[] ps = new Philo[ 5 ]; Stick[] ss = new Stick[ 5 ]; for ( int i = 0 ; i < 5 ; i++) { ss[i] = new Stick( true , i); ps[i]= new Philo(i, 0 ); } System.out.println(( 0 - 1 )% 5 ); for ( int i = 0 ; i < 5 ; i++) { ps[i].leftPhilo= ps[(i+ 4 )% 5 ]; ps[i].rightPhilo= ps[(i+ 1 )% 5 ]; ps[i].leftStick = ss[i]; ps[i].rightStick=ss[(i+ 1 )% 5 ]; } for ( int i = 0 ; i < 5 ; i++) { exec.execute(ps[i]); } exec.shutdown(); }} 結果根據哲學家的需求和當時的情況進行分配而不是人為的結果。