Javaにおける食事哲学者問題
1前書き
Dining Philosophers問題は、マルチスレッド環境での同期の問題を説明し、それらを解決するための手法を説明するために使用される古典的な問題の1つです。 Dijkstraは最初にこの問題を定式化し、テープドライブ周辺機器にアクセスするコンピュータに関してそれを提示しました。
本定式化は、クイックソート分類アルゴリズムを発明したことでも知られているTony Hoareによって与えられた。この記事では、このよく知られた問題を分析し、一般的な解決策をコーディングします。
2問題
リンク:/uploads/dp0.png%20662w[]
上の図は問題を表しています。 5人のサイレント哲学者(P1 – P5)が円形のテーブルの周りに座って、食事と思考に費やしています。
彼らが共有するためには5つのフォークがあり(1 – 5)、食べることができるように、哲学者は彼の両手にフォークを持つ必要があります。食べた後、彼はそれらの両方を置き、そしてそれらは同じサイクルを繰り返す他の哲学者によって選ばれることができます。
目標は、哲学者が餓死することなく食事と思考の彼らの目標を達成するのを助ける計画/プロトコルを思いつくことである。
===
3解決策
最初の解決策は、各哲学者を以下のプロトコルに従わせることです。
while(true) { //Initially, thinking about life, universe, and everything think(); //Take a break from thinking, hungry now pick__up__left__fork(); pick__up__right__fork(); eat(); put__down__right__fork(); put__down__left__fork(); //Not hungry anymore. Back to thinking! }
上記の擬似コードが説明しているように、各哲学者は最初に考えています。
ある程度の時間が経つと、哲学者は空腹になり、食べたいと思います。
この時点で、彼はどちらかの側のフォークに手を伸ばし、両方を手に入れたら、食べることになります** 。食事が終わると、哲学者はフォークを下ろして、隣人が使えるようにします。
===
4実装
私たちはそれぞれの哲学者を
Runnable
インターフェースを実装するクラスとしてモデル化し、それらを別々のスレッドとして実行できるようにします。それぞれの
Philosopher
は、彼の左右両側にある2つのフォークにアクセスできます。public class Philosopher implements Runnable { //The forks on either side of this Philosopher private Object leftFork; private Object rightFork; public Philosopher(Object leftFork, Object rightFork) { this.leftFork = leftFork; this.rightFork = rightFork; } @Override public void run() { //Yet to populate this method } }
私たちには、行動を実行するように
Philosopher
に指示する方法もあります – 食べることに備えてフォークを食べる、考える、または取得する:public class Philosopher implements Runnable { //Member variables, standard constructor private void doAction(String action) throws InterruptedException { System.out.println( Thread.currentThread().getName() + " " + action); Thread.sleep(((int) (Math.random() ** 100))); } //Rest of the methods written earlier }
上記のコードに示すように、各アクションは呼び出しスレッドをランダムな時間中断することによってシミュレートされるため、実行順序は時間だけでは強制されません。
それでは、「哲学者」のコアロジックを実装しましょう。
フォークの取得をシミュレートするには、2つの
Philosopher
スレッドが同時にそれを取得しないようにフォークをロックする必要があります。これを実現するために、これを実現するには、
synchronized
キーワードを使用してforkオブジェクトの内部モニタを取得し、他のスレッドが同じことをしないようにします。 Javaの
synchronized
キーワードの手引きは
ここ
にあります。
Philosopher
クラスに
run()
メソッドを実装します。public class Philosopher implements Runnable { //Member variables, methods defined earlier @Override public void run() { try { while (true) { //thinking doAction(System.nanoTime() + ": Thinking"); synchronized (leftFork) { doAction( System.nanoTime() + ": Picked up left fork"); synchronized (rightFork) { //eating doAction( System.nanoTime() + ": Picked up right fork - eating"); doAction( System.nanoTime() + ": Put down right fork"); } //Back to thinking doAction( System.nanoTime() + ": Put down left fork. Back to thinking"); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }
この計画はまさに先に述べたものを実装しています。
この後、彼は彼の左右にフォークを取得して食べ始めます。完了したら、彼はフォークを下に置きます。また、各アクションにタイムスタンプを追加しているので、イベントが発生する順序を理解するのに役立ちます。
プロセス全体をキックスタートするために、5つの
Philosophers
をスレッドとして作成し、それらすべてを起動するクライアントを作成します。public class DiningPhilosophers { public static void main(String[]args) throws Exception { Philosopher[]philosophers = new Philosopher[5]; Object[]forks = new Object[philosophers.length]; for (int i = 0; i < forks.length; i++) { forks[i]= new Object(); } for (int i = 0; i < philosophers.length; i++) { Object leftFork = forks[i]; Object rightFork = forks[(i + 1) % forks.length]; philosophers[i]= new Philosopher(leftFork, rightFork); Thread t = new Thread(philosophers[i], "Philosopher " + (i + 1)); t.start(); } } }
私たちは各フォークを一般的なJavaオブジェクトとしてモデル化し、哲学者と同じ数だけそれらを作ります。私たちは、彼が左右しようとしているそれぞれの左右のフォークに、同期化キーワードを使ってロックしようとしています。
このコードを実行すると、次のような出力が得られます。おそらく
sleep()
メソッドが異なる間隔で呼び出されるためです。Philosopher 1 8038014601251: Thinking Philosopher 2 8038014828862: Thinking Philosopher 3 8038015066722: Thinking Philosopher 4 8038015284511: Thinking Philosopher 5 8038015468564: Thinking Philosopher 1 8038016857288: Picked up left fork Philosopher 1 8038022332758: Picked up right fork - eating Philosopher 3 8038028886069: Picked up left fork Philosopher 4 8038063952219: Picked up left fork Philosopher 1 8038067505168: Put down right fork Philosopher 2 8038089505264: Picked up left fork Philosopher 1 8038089505264: Put down left fork. Back to thinking Philosopher 5 8038111040317: Picked up left fork
すべての
__Philosopher
は最初は思考から始め、
Philosopher 1__が左右のフォークを拾い上げ、それから両方を下に向けて食べ、そしてその後にPhilosopher 5が拾い上げます。===
5解決策の問題:デッドロック
上記の解決策は正しいようですが、デッドロックが発生するという問題があります。
デッドロックは、各プロセスが他のプロセスによって保持されているリソースの獲得を待っているときにシステムの進行が停止される状況である。
上記のコードを数回実行し、場合によってはコードがハングアップすることを確認することで、同じことを確認できます。上記の問題を示す出力例は次のとおりです。
Philosopher 1 8487540546530: Thinking Philosopher 2 8487542012975: Thinking Philosopher 3 8487543057508: Thinking Philosopher 4 8487543318428: Thinking Philosopher 5 8487544590144: Thinking Philosopher 3 8487589069046: Picked up left fork Philosopher 1 8487596641267: Picked up left fork Philosopher 5 8487597646086: Picked up left fork Philosopher 4 8487617680958: Picked up left fork Philosopher 2 8487631148853: Picked up left fork
この状況では、それぞれの
__Philosopher
__は彼の左フォークを獲得しましたが、彼の隣人はすでにそれを獲得したので、彼の右フォークを獲得することはできません。この状況は一般に「循環待機」として知られており、デッドロックを引き起こしてシステムの進行を妨げる条件の1つです。====
6. デッドロックの解決
前述したように、デッドロックの主な理由は、各プロセスが他のプロセスによって保持されているリソースを待機する循環待機状態です。したがって、デッドロック状態を回避するには、循環待機状態が確実に破られるようにする必要があります。これを実現する方法はいくつかありますが、最も簡単な方法は次のとおりです。
最初に右のフォークに手を伸ばす人を除いて、すべての哲学者は最初に彼らの左のフォークに手を伸ばす。
私たちは、コードに比較的小さな変更を加えることによって、これを既存のコードに実装します。
public class DiningPhilosophers { public static void main(String[]args) throws Exception { final Philosopher[]philosophers = new Philosopher[5]; Object[]forks = new Object[philosophers.length]; for (int i = 0; i < forks.length; i++) { forks[i]= new Object(); } for (int i = 0; i < philosophers.length; i++) { Object leftFork = forks[i]; Object rightFork = forks[(i + 1) % forks.length]; if (i == philosophers.length - 1) { //The last philosopher picks up the right fork first philosophers[i]= new Philosopher(rightFork, leftFork); } else { philosophers[i]= new Philosopher(leftFork, rightFork); } Thread t = new Thread(philosophers[i], "Philosopher " + (i + 1)); t.start(); } } }
この変更は、上のコードの17行目から19行目にあります。ここでは、最後の哲学者が左ではなく右のフォークに最初に到達する条件を紹介します。これは循環待機状態を破り、デッドロックを回避することができます。
以下の出力は、すべての
__Philosopher
__がデッドロックを引き起こすことなく、考えて食べて食べる機会を得たケースの1つを示しています。Philosopher 1 88519839556188: Thinking Philosopher 2 88519840186495: Thinking Philosopher 3 88519840647695: Thinking Philosopher 4 88519840870182: Thinking Philosopher 5 88519840956443: Thinking Philosopher 3 88519864404195: Picked up left fork Philosopher 5 88519871990082: Picked up left fork Philosopher 4 88519874059504: Picked up left fork Philosopher 5 88519876989405: Picked up right fork - eating Philosopher 2 88519935045524: Picked up left fork Philosopher 5 88519951109805: Put down right fork Philosopher 4 88519997119634: Picked up right fork - eating Philosopher 5 88519997113229: Put down left fork. Back to thinking Philosopher 5 88520011135846: Thinking Philosopher 1 88520011129013: Picked up left fork Philosopher 4 88520028194269: Put down right fork Philosopher 4 88520057160194: Put down left fork. Back to thinking Philosopher 3 88520067162257: Picked up right fork - eating Philosopher 4 88520067158414: Thinking Philosopher 3 88520160247801: Put down right fork Philosopher 4 88520249049308: Picked up left fork Philosopher 3 88520249119769: Put down left fork. Back to thinking
コードを数回実行することで、システムが以前に発生したデッドロック状況から解放されたことを確認できます。
===
7. 結論
この記事では、有名なDining Philosophers問題と、循環待機およびデッドロックの概念について説明しました。デッドロックを引き起こす簡単な解決策をコーディングし、循環待機を中断してデッドロックを回避するように単純な変更を加えました。これはほんの始まりに過ぎず、より洗練されたソリューションが存在します。
この記事のコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-concurrency[over on GitHub]にあります。