
コンピュータ サイエンスにおいて、食事哲学者問題は、同期の問題とその解決手法を説明するために並行アルゴリズムの設計でよく使用される例題です。
これはもともと1965年にエドガー・ダイクストラによって学生の試験問題として考案され、テープドライブ周辺機器へのアクセスをめぐるコンピュータの競合という形で提示されました。その後すぐに、トニー・ホーアが現在の形の問題を考案しました。[1] [2] [3] [4]
問題の説明
5 人の哲学者が同じテーブルで食事をしています。各哲学者はテーブルに自分の皿を持っています。各皿の間にはフォークがあります。出された料理は一種のスパゲッティで、2 本のフォークで食べなければなりません。各哲学者は、考えることと食べることを交互にしか行うことができません。さらに、哲学者は左右のフォークの両方を持っているときにのみスパゲッティを食べることができます。したがって、2 本のフォークは、最も近い 2 人の隣人が食べているときではなく、考えているときにのみ使用できます。個々の哲学者は食事を終えると、両方のフォークを置きます。問題は、どの哲学者も飢えることのないようなレジメン (並行アルゴリズム) をどのように設計するかです。つまり、どの哲学者も、他の人がいつ食べたり考えたりしたいかを知ることができないと仮定して、各哲学者が食べることと考えることを永遠に交互に続けることができるようにします (不完全情報の問題)。
問題点
この問題は、デッドロック(進行不可能なシステム状態)を回避する難しさを示すために設計されました。この問題の適切な解決法が明らかでないことを確認するために、各哲学者に次のように行動するよう指示する提案を検討してください。
- 左のフォークが利用可能になるまで考え、利用可能になったらそれを拾います。
- 適切なフォークが利用可能になるまで考え続け、利用可能になったらそれを手に取ります。
- 両方のフォークを握った状態で、一定時間食べます。
- 左のフォークを下ろす。
- 右のフォークを下ろす。
- 最初から繰り返します。
これらの指示により、各哲学者がフォークを左側に持つ状況が発生する可能性があります。その状況では、哲学者全員がもう一方のフォークが利用可能になるまで永遠に待機することになります。これはデッドロックです。
リソース不足、相互排他、ライブロックは、シーケンスおよびアクセスの問題の他のタイプです。
ソリューション
デッドロックが発生するには、相互排他性(複数の哲学者が同時にフォークを使用することはできない)、リソース保持(哲学者は 2 番目のフォークを待機している間、1 つのフォークを保持する)、非プリエンプション(哲学者は他の哲学者からフォークを取得できない)、循環待機(各哲学者は左隣の哲学者を待機している可能性がある) の 4 つの条件が必要です。ソリューションは、これら 4 つの条件のうち少なくとも 1 つを否定する必要があります。実際には、相互排他性または非プリエンプションを否定することで、何らかの形で有効なソリューションが得られますが、ほとんどの理論的な処理では、これらの仮定は交渉の余地がないと想定し、代わりにリソース保持または循環待機 (多くの場合、両方) を攻撃します。
ダイクストラの解
ダイクストラのソリューションはリソースの保持を否定します。哲学者は両方のフォークをアトミックに取得するか待機し、クリティカルセクションの外側で正確に1つのフォークを保持することはありません。これを実現するために、ダイクストラのソリューションは、哲学者ごとに1つのミューテックス、1つのセマフォ、および哲学者ごとに1つの状態変数を使用します。このソリューションは、リソース階層ソリューションよりも複雑です。[5] [4]これは、タネンバウムの変更を加えたダイクストラのソリューションのC++20バージョンです。
#include <chrono> #include <iostream> #include <mutex> #include <random> #include <semaphore> #include <thread>
constexpr const size_t N = 5 ; // 哲学者(およびフォーク)の数enum class State { THINKING = 0 、// 哲学者は考えている THINKING HUNGRY = 1 、// 哲学者はフォークを手に入れようとしているEATING = 2 、// 哲学者は食べている};
size_t inline left ( size_t i ) { // 両方のフォークが利用可能な哲学者 i の左隣の人の番号return ( i - 1 + N ) % N ; // i - 1 が負の場合に N が追加されます}
size_t inline right ( size_t i ) { // 両方のフォークが利用可能な哲学者 i の右隣の番号return ( i + 1 ) % N ; }
State state [ N ]; // 全員の both_forks_available 状態を追跡する配列
std :: mutex critical_region_mtx ; // クリティカル領域の相互排他制御// (フォークの持ち上げと置き) std :: mutex output_mtx ; // 同期された cout (THINKING/HUNGRY/EATING ステータスの出力)
// バイナリ セマフォの配列。哲学者ごとに 1 つのセマフォ。//
取得されたセマフォは、哲学者 i が 2 つのフォークを取得 (ブロック) したことを意味します
std :: binary_semaphore both_forks_available [ N ] { std :: binary_semaphore { 0 } , std :: binary_semaphore { 0 }, std :: binary_semaphore { 0 }, std :: binary_semaphore { 0 }, std :: binary_semaphore { 0 } };
size_t my_rand ( size_t min 、size_t max ) { static std :: mt19937 rnd ( std :: time ( nullptr )); return std :: universe_int_distribution <> ( min 、max )( rnd ); }
void test ( size_t i ) // 哲学者 i が空腹で、両方の隣人が食べていない場合は、 eat { // i: 哲学者番号、0 から N-1 までif ( state [ i ] == State :: HUNGRY && state [ left ( i )] != State :: EATING && state [ right ( i )] != State :: EATING ) { state [ i ] = State :: EATING ; both_forks_available [ i ]. release (); // この eat セッションではフォークは不要になりました} }
void think ( size_t i ) { size_t duration = my_rand ( 400 , 800 ); { std :: lock_guard < std :: mutex > lk ( output_mtx ); // 中断されないクリティカル セクション print std :: cout << i << " is thinking " << duration << "ms \n " ; } std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( duration )); }
void take_forks ( size_t i ) { { std :: lock_guard < std :: mutex > lk { critical_region_mtx }; // クリティカル領域に入るstate [ i ] = State :: HUNGRY ; // 哲学者 i が State::HUNGRY であるという事実を記録する{ std :: lock_guard < std :: mutex > lk ( output_mtx ); // 中断のないクリティカル セクション print std :: cout << " \t\t " << i << " is State::HUNGRY \n " ; } test ( i ); // 2 つのフォーク (の許可) を取得しようとする} // クリティカル領域を終了するboth_forks_available [ i ]. acquire (); // フォークが取得されなかった場合はブロックする}
void eat ( size_t i ) { size_tduration = my_rand ( 400 , 800 ); { std :: lock_guard < std :: mutex > lk ( output_mtx ); // 中断されない印刷のためのクリティカル セクションstd :: cout << " \t\ t \t\t " << i << " は " << duration << "msを消費しています\n " ; } std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( duration )); }
void put_forks ( size_t i ) { std :: lock_guard < std :: mutex > lk { critical_region_mtx }; // クリティカル領域に入るstate [ i ] = State :: THINKING ; // 哲学者は State::EATING を終了しましたtest ( left ( i )); // 左隣が食べられるようになったか確認test ( right ( i )); // 右隣が食べられるようになったか確認// 関数を終了してクリティカル領域を終了します}
void philosopher ( size_t i ) { while ( true ) { // 永遠に繰り返すthink ( i ); // philosopher は State::THINKING であるtake_forks ( i ); // 2 つのフォークを取得するかブロックするeat ( i ); // おいしい、スパゲッティput_forks ( i ); // 両方のフォークをテーブルに戻し、隣の人が食べられるかどうか確認する} }
int main () { std :: cout << "dp_14 \n " ;
std :: jthread t0 ([ & ] { philosopher ( 0 ); }); // [&] は、後続のラムダの外側にあるすべての変数を意味しますstd :: jthread t1 ([ & ] { philosopher ( 1 ) ; }); // 参照によってキャプチャされますstd :: jthread t2 ([ & ] { philosopher ( 2 ); }); std :: jthread t3 ([ & ] { philosopher ( 3 ); }); std :: jthread t4 ([ & ] { philosopher ( 4 ); }); }
関数 test() と、それを take_forks() および put_forks() で使用すると、Dijkstra ソリューションはデッドロックフリーになります。
リソース階層ソリューション
このソリューションは、リソース (この場合はフォーク) に部分的な順序を割り当てることによって循環的な待機を無効にし、すべてのリソースが順番に要求され、順序が関連していない 2 つのリソースが単一の作業単位で同時に使用されることはないという規則を確立します。ここでは、リソース (フォーク) に 1 から 5 の番号が付けられ、各作業単位 (哲学者) は、使用を計画している 2 本のフォークの中から、常に最初に番号の小さいフォークを取り、次に番号の大きいフォークを取ります。各哲学者がフォークを置く順序は重要ではありません。この場合、5 人の哲学者のうち 4 人が同時に番号の小さいフォークを手に取ると、テーブルには番号の大きいフォークだけが残るため、5 人目の哲学者はフォークを手に取ることができません。さらに、番号の大きいフォークにアクセスできるのは 1 人の哲学者だけなので、2 本のフォークを使って食べることができます。これは直感的に、テーブルに「左利き」の哲学者が 1 人いて、他のすべての哲学者とは異なり、最初に左からフォークを取るようなものと考えることができます。
リソース階層ソリューションはデッドロックを回避しますが、特に必要なリソースのリストが事前に完全にわかっていない場合には、常に実用的であるとは限りません。たとえば、作業単位がリソース 3 と 5 を保持していて、リソース 2 が必要であると判断した場合、5 を解放し、次に 3 を解放してから 2 を取得し、その後 3 と 5 をこの順序で再取得する必要があります。大量のデータベース レコードにアクセスするコンピュータ プログラムは、新しいレコードにアクセスする前にすべての大きい番号のレコードを解放する必要があると、効率的に実行されず、その目的には実用的ではありません。[2]
リソース階層ソリューションは公平ではありません。哲学者 1 がフォークを取るのが遅く、哲学者 2 が素早く考えてフォークを拾い上げた場合、哲学者 1 は両方のフォークを拾うことができません。公平なソリューションは、各哲学者が他の哲学者に比べてどれだけゆっくり動いても、最終的には各哲学者が食べることを保証する必要があります。
以下のソースコードは、5人の哲学者のためのリソース階層ソリューションのC++11実装です。sleep_for()関数は、通常ビジネスロジックに費やされる時間をシミュレートします。[6]
GCCの場合: コンパイル
g++ src.cpp -std = c++11 -lpthread
#include <iostream> #include <chrono> #include <mutex> #include <thread> #include <random> #include <ctime>
名前空間std を使用します。
int myrand ( int min , int max ) { static mt19937 rnd ( time ( nullptr )); return universe_int_distribution <> ( min , max )( rnd ); }
void philosopher ( int ph , mutex & ma , mutex & mb , mutex & mo ) { for (;;) { // スレッドの終了を防ぐint duration = myrand ( 200 , 800 ); { // Block { } はロックのスコープを制限しますlock_guard < mutex > gmo ( mo ); cout << ph << " thinks " << duration << "ms \n " ; } this_thread :: sleep_for ( chrono :: milliseconds ( duration )); { lock_guard < mutex > gmo ( mo ); cout << " \t\t " << ph << " is hungry \n " ; } lock_guard < mutex > gma ( ma ); // sleep_for() ここで 2 番目のフォークを探す前の遅延を追加できますが、必須ではありません。lock_guard < mutex > gmb ( mb ); duration = myrand ( 200 , 800 ); { lock_guard < mutex > gmo ( mo ); cout << " \t\t\t\t " << ph << " eats " << duration << "ms \n " ; } this_thread :: sleep_for ( chrono :: milliseconds ( duration ) ); } }
int main () { cout << "dining Philosophers C++11 with Resource hierarchy \n " ; mutex m1 , m2 , m3 , m4 , m5 ; // 5 つのフォークは 5 つのミューテックスですmutex mo ; // 適切な出力のため// 5 人の哲学者は 5 つのスレッドですthread t1 ([ & ] { philosopher ( 1 , m1 , m2 , mo );}); thread t2 ([ & ] { philosopher ( 2 , m2 , m3 , mo );}); thread t3 ([ & ] { philosopher ( 3 , m3 , m4 , mo );}); thread t4 ([ & ] { philosopher ( 4 , m4 , m5 , mo );}); thread t5 ( [ & ] { philosopher ( 5 , m1 , m5 , mo );}); // リソース階層を強制するt1.join ( ) ; // スレッドの終了を防ぐt2.join (); t3.join ( ) ; t4.join ( ) ; t5.join ( ) ; }
仲裁人による解決策
もう 1 つのアプローチは、循環的な待機に代わる仲裁者 (ウェイターなど) を導入して、哲学者が両方のフォークを拾うか、まったく拾わないかのいずれかしか選択できないようにすることです。哲学者はフォークを拾うために、ウェイターに許可を求めなければなりません。哲学者が両方のフォークを拾うまで、ウェイターは一度に 1 人の哲学者にのみ許可を与えます。フォークを置くことは常に許可されています。ウェイターはミューテックスとして実装できます。新しい中心的存在 (ウェイター) を導入することに加えて、このアプローチは並列性の低下をもたらす可能性があります。哲学者が食事をしていて、隣の哲学者の 1 人がフォークを要求している場合、他のすべての哲学者は、自分たちのフォークがまだ利用可能であっても、この要求が満たされるまで待たなければなりません。
テーブルに座る人数を制限する
ウィリアム・スタリングス[7]が提案した解決策は、最大でn-1 人の哲学者がいつでも着席できるようにするというものです。最後の哲学者は、誰かが食事を終えてから「着席」してフォークへのアクセスを要求する (たとえば、セマフォを使用して) 必要があります。これにより循環的な待機がなくなり、少なくとも 1 人の哲学者が常に両方のフォークを取得できることが保証され、システムが進行できるようになります。
シャンディ/ミスラの解決策
1984年、K.マニ・チャンディとJ.ミスラ[8]は、ダイクストラの解決策とは異なり、任意のエージェント(番号P 1、...、P n)が任意の数のリソースを争うことを可能にする、食事哲学者問題に対する別の解決策を提案しました。この解決策も完全に分散されており、初期化後に中央機関を必要としません。ただし、これは「哲学者が互いに話さない」という要件に違反しています(リクエストメッセージのため)。
- リソースを争う哲学者のペアごとに、フォークを作成し、ID が低い方の哲学者に渡します (エージェントP nの場合はn )。各フォークはダーティまたはクリーンのいずれかになります。最初は、すべてのフォークがダーティです。
- 哲学者がリソース セット (つまり、食べる) を使用したい場合、その哲学者は競合する近隣の哲学者からフォークを取得する必要があります。哲学者は、自分が持っていないすべてのフォークに対して、要求メッセージを送信します。
- フォークを持った哲学者がリクエストメッセージを受け取ると、フォークがきれいであればそれを保持しますが、汚れている場合はそれを手放します。哲学者がフォークを送る場合は、送る前にフォークをきれいにします。
- 哲学者が食事を終えると、彼のフォークはすべて汚れます。他の哲学者が以前にフォークの 1 つを要求していた場合、食事を終えたばかりの哲学者がフォークをきれいにして送ります。
このソリューションは、高度な同時実行性も可能にし、任意の規模の問題を解決します。
また、飢餓の問題も解決します。クリーン/ダーティ ラベルは、最も「飢餓状態」にあるプロセスを優先し、「食べた」ばかりのプロセスに不利な状況を与える方法として機能します。彼らのソリューションは、哲学者が 2 回続けて食べると、その間に他の人にフォークを使わせることができないというソリューションに似ています。Chandy と Misra のソリューションはそれよりも柔軟ですが、その方向に傾く要素があります。
彼らの分析では、フォークの分布とそれらのクリーン/ダーティ状態から優先レベルのシステムを導出しています。彼らは、このシステムが有向非巡回グラフを記述する可能性があり、そうである場合、彼らのプロトコルの操作はそのグラフを巡回グラフにすることはできないことを示しています。これにより、循環待機を否定することでデッドロックが発生しないことが保証されます。ただし、すべての哲学者が左側のフォークを保持しているなど、システムが完全に対称な状態に初期化されている場合、グラフは最初から巡回的であり、彼らのソリューションではデッドロックを防ぐことができません。より低い ID を持つ哲学者がダーティフォークを持つようにシステムを初期化すると、グラフが最初は非巡回であることが保証されます。
参照
参考文献
- ^ Dijkstra, Edsger W. EWD-1000 (PDF) . EW Dijkstra アーカイブ.テキサス大学オースティン校アメリカ歴史センター.(書き起こし)
- ^ ab J. Díaz; I. Ramos (1981). プログラミング概念の形式化: 国際コロキウム、ペニスコラ、スペイン、1981 年 4 月 19 ~ 25 日。議事録。Birkhäuser。pp. 323、326。ISBN 978-3-540-10699-9。
- ^ Hoare, CAR (2004) [1985 年に Prentice Hall International によって最初に出版]. 「Communicating Sequential Processes」(PDF) . usingcsp.com.
- ^ ab Tanenbaum, Andrew S. (2006)、『オペレーティングシステム - 設計と実装』第3版 [章: 2.3.1 食事する哲学者の問題]、Pearson Education、Inc.
- ^ Dijkstra, Edsger W. EWD-310 (PDF) . EW Dijkstra アーカイブ.テキサス大学オースティン校アメリカ歴史センター.(書き起こし)
- ^ Tanenbaum, Andrew S. (2006)、『オペレーティングシステム - 設計と実装』第 3 版 [章: 3.3.5 デッドロック防止]、Pearson Education、Inc.
- ^ Stallings, William (2018). オペレーティングシステム: 内部と設計原則 (第9版). ハーロウ、エセックス、イギリス:ピアソン. p. 310. ISBN 978-1-292-21429-0. OCLC 1009868379.
- ^ Chandy, KM; Misra, J. (1984). 飲酒哲学者問題。プログラミング言語およびシステムに関する ACM トランザクション。
文献
- シルバーシャッツ、アブラハム、ピーターソン、ジェームズ L. (1988)。オペレーティングシステムの概念。Addison- Wesley。ISBN 0-201-18760-4。
- Dijkstra, EW (1971年6月)。「シーケンシャルプロセスの階層的順序付け」Acta Informatica 1(2): 115–138。
- Lehmann, DJ、Rabin M. O、(1981)。自由選択の利点について: 食事する哲学者問題に対する対称的かつ完全に分散されたソリューション。プログラミング言語の原則 1981 ( POPL '81)、pp. 133–138。
外部リンク
- 2人または4人の哲学者のための解決コード付き問題の議論 2011-07-20にWayback Machineでアーカイブされました
- Wayback Machineでのさまざまな解決策に関する議論(2013 年 12 月 8 日アーカイブ)
- 継続ベースのスレッド (cbthreads) を使用したソリューションに関するWayback Machineでの議論(2012 年 3 月 4 日アーカイブ)
- TLA+で記述されたChandy-Misraソリューションの形式仕様
- 分散対称ソリューション
- シミュレーションによる食事哲学者のプログラミング
- 哲学者の問題のインタラクティブな例 (Java が必要)
- サタンが夕食にやってくる
- Wot No Chickens? – Peter H. Welch は、Java スレッド モニターの動作の残念な結果として、スレッドの飢餓が厳密に必要な場合よりも起こりやすくなることを示す Starving Philosophers バリアントを提案しました。
- スレッドメンター
- 非同期エージェントによる食事哲学者問題の解決
- アクターを使ったソリューション
