21
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[翻訳]Rustを学ぼう - 食事する哲学者

Last updated at Posted at 2015-06-10

Rustドキュメントの日本語訳は Rustドキュメント翻訳プロジェクト に移行しました。

http://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/dining-philosophers.html にてRust 1.6原文に追従させています。

追記: "Dining Philosophers"を含む"Learn Rust"セクションは、Rust 1.7で削除されました。経緯は #30595 Remove "Learn Rust" from TRPL. 参照。


Rust 1.0時点の Learn Rust - Dining Philosophers より訳出。

訳者注:Qiita Markdownソースには原文をコメント形式<!--/-->で残してあります。誤訳修正や訳出改善の編集リクエストを歓迎します。


食事する哲学者(Dining Philosophers)

私たちの2番目のプロジェクトとして、古典的な並行処理問題を考えていきましょう。「食事する哲学者(the dining philosophers)」とよばれる問題です。オリジナルは1965年にダイクストラ(Dijkstra)により考案されましたが、ここではトニー・ホーア(Tony Hoare)による1985年のこの論文バージョンを用います。

昔々、裕福な慈善家が、5人の高名な哲学者が宿泊できるカレッジを寄付しました。それぞれの哲学者には思索活動にふさわしい部屋が与えられました;また共用のダイニングルームもあり、そこには丸いテーブルが置かれ、5人それぞれが専用で使うイス5脚で取り囲まれていました。彼らはテーブルを反時計回りに座ります。哲学者の左側にはそれぞれ金のフォークが配され、中央には大きなボウルに入ったスパゲッティが常に補充されていました。哲学者は大半の時間を思慮に費やすのですが;空腹になった時は、ダイニングルームに出向き、自分専用のイスに座り、左側のフォークを取上げ、スパゲッティに突き刺します。しかし、絡まり合ったスパゲッティを口元まで運ぶには2本目のフォークが必要でした。そこで哲学者は自分の右側にあるフォークも使うことにしたのです。食べ終わったら両側のフォークを元に戻し、席から立ちあがって、思索活動を続けます。もちろん、1本のフォークは同時に1人の哲学者しか使えません。他の哲学者が食事したければ、フォークが再び戻されるまで待たねばなりません。

この古典問題は並行処理特有の要因を際立たせます。その実装にあたっては少し注意が必要です: 単純な実装ではデッドロックの可能性があるのです。例えば、この問題を解く単純なアルゴリズムを考えてみましょう:

  1. 哲学者は左側のフォークを取上げます。
  2. 続いて右側のフォークを取上げます。
  3. 食事をします。
  4. 2本のフォークを戻します。

さて、このようなイベント順を想像してみましょう:

  1. 1番目の哲学者は、アルゴリズムに従って左側のフォークを取上げます。
  2. 2番目の哲学者は、アルゴリズムに従って左側のフォークを取上げます。
  3. 3番目の哲学者は、アルゴリズムに従って左側のフォークを取上げます。
  4. 4番目の哲学者は、アルゴリズムに従って左側のフォークを取上げます。
  5. 5番目の哲学者は、アルゴリズムに従って左側のフォークを取上げます。
  6. ...?全てのフォークが取られたのに、誰も食事できません!

この事態を解決する方法はいくつかあります。チュートリアルでは独自の解法をとります。それでは、問題をモデル化するところから始めましょう。哲学者から手を付けていきます:

struct Philosopher {
    name: String,
}

impl Philosopher {
    fn new(name: &str) -> Philosopher {
        Philosopher {
            name: name.to_string(),
        }
    }
}

fn main() {
    let p1 = Philosopher::new("Baruch Spinoza");
    let p2 = Philosopher::new("Gilles Deleuze");
    let p3 = Philosopher::new("Karl Marx");
    let p4 = Philosopher::new("Friedrich Nietzsche");
    let p5 = Philosopher::new("Michel Foucault");
}

ここでは、哲学者を表すstruct(構造体)を作ります。まずは名前だけで十分でしょう。名前には&str型ではなくString型を選びました。一般的に、データを所有(own)する型を用いた方が、参照型(references)の利用よりも簡単になります。

では続けましょう:

impl Philosopher {
    fn new(name: &str) -> Philosopher {
        Philosopher {
            name: name.to_string(),
        }
    }
}

このimplブロックはPhilosopher構造体に関する定義を与えます。ここでは、newという「関連する関数(associated function)」を定義します。最初の行は次の通りです:

fn new(name: &str) -> Philosopher {

関数は&str型の引数1つ、nameをとります。これは他の文字列への参照(reference)です。そしてPhilosopher構造体のインスタンスを返します。

Philosopher {
    name: name.to_string(),
}

関数は新しいPhilosopherインスタンスを作成し、そのnameフィールドに引数nameを設定します。ここでは引数を直接設定するのではなく、.to_string()を呼び出しています。これにより&strが指す文字列のコピーが作られ、Philosophernameフィールド型に合わせた新しいStringがえられます。

なぜ引数に直接Stringを受付けないのか?いい質問です。仮にStringをとるとしたら、呼出し元は&str値をもっていますから、呼出元でメソッドを呼ぶ必要がでしまいます。この利便性の代償として、_常に_コピーが作られてしまいます。今回のプログラムでは、短い文字列しか与えないことが分かっているため、これは大して重要な問題ではありません。

最後の注意点として: ここはPhilosopherを定義しただけで、何もしていないように見えます。Rust言語は「式ベース(expression based)」なので、Rustではほとんどが値を返す式となります。関数についても同じことが言え、最後の式が自動的に戻り値となります。ここでは関数内の最後の式で新しいPhilosopherを生成し、それを戻り値としているのです。

このnew()という名前、Rustにとって特別な意味を持ちませんが、構造体の新しいインスタンスを生成する関数としてよく用いられます。その理由について話す前に、再びmain()を見ていきましょう:

fn main() {
    let p1 = Philosopher::new("Baruch Spinoza");
    let p2 = Philosopher::new("Gilles Deleuze");
    let p3 = Philosopher::new("Karl Marx");
    let p4 = Philosopher::new("Friedrich Nietzsche");
    let p5 = Philosopher::new("Michel Foucault");
}

ここでは、5つの新しい哲学者に対して5つの変数束縛を作ります。お気に入りの5人なのですが、自由に置き換えて構いませんよ1。仮にnew関数を定義_しない_なら、次ように書く必要があります:

fn main() {
    let p1 = Philosopher { name: "Baruch Spinoza".to_string() };
    let p2 = Philosopher { name: "Gilles Deleuze".to_string() };
    let p3 = Philosopher { name: "Karl Marx".to_string() };
    let p4 = Philosopher { name: "Friedrich Nietzche".to_string() };
    let p5 = Philosopher { name: "Michel Foucault".to_string() };
}

これでは面倒ですよね。newの利用には他の利点もありますが、今回のような単純なケースでも、利用側コードをシンプルにできるのです。

下準備が終わったところで、取り組むべき広い問題がいくつか出てきました。まずは逆順に: 哲学者が食事を終わらせる部分から始めていきましょう。この小さなステップでは、メソッドを1つ作り、全ての哲学者に対して呼び出します:

struct Philosopher {
    name: String,
}   

impl Philosopher { 
    fn new(name: &str) -> Philosopher {
        Philosopher {
            name: name.to_string(),
        }
    }

    fn eat(&self) {
        println!("{} is done eating.", self.name);
    }
}

fn main() {
    let philosophers = vec![
        Philosopher::new("Baruch Spinoza"),
        Philosopher::new("Gilles Deleuze"),
        Philosopher::new("Karl Marx"),
        Philosopher::new("Friedrich Nietzsche"),
        Philosopher::new("Michel Foucault"),
    ];

    for p in &philosophers {
        p.eat();
    }
}

最初はmain()から見ていきます。哲学者たちに5つの変数束縛を個別に行うのではなく、代わりにVec<T>を用いました。Vec<T>は「ベクトル(vector)」とも呼ばれる、可変長の配列型です。ベクトルの走査にはforループを使っているため、それぞれの哲学者への参照(reference)が順番にえられます。

ループ本体の中では、上記で定義したp.eat()を呼び出します:

fn eat(&self) {
    println!("{} is done eating.", self.name);
}

Rustでは、メソッド(methods)は明示的なselfパラメータを取ります。なのでeat()はメソッドとなり、new()selfを取らないため関連する関数(associated function)となります。最初のeat()バージョンでは、哲学者の名前と、食事が終わったことを表示するだけです。このプログラムを実行すると次の出力がえられるはずです:

Baruch Spinoza is done eating.
Gilles Deleuze is done eating.
Karl Marx is done eating.
Friedrich Nietzsche is done eating.
Michel Foucault is done eating.

これだけなら簡単ですね!私たちはまだ真の問題を実装していませんから、実際のところは何も出来ていませんけど!

続いて、哲学者たちが一瞬で食事を終えるのではなく、本当に食事するようにしたいです。これが次のバージョンです:

use std::thread;

struct Philosopher {
    name: String,
}   

impl Philosopher { 
    fn new(name: &str) -> Philosopher {
        Philosopher {
            name: name.to_string(),
        }
    }
    
    fn eat(&self) {
        println!("{} is eating.", self.name);

        thread::sleep_ms(1000);

        println!("{} is done eating.", self.name);
    }
}

fn main() {
    let philosophers = vec![
        Philosopher::new("Baruch Spinoza"),
        Philosopher::new("Gilles Deleuze"),
        Philosopher::new("Karl Marx"),
        Philosopher::new("Friedrich Nietzsche"),
        Philosopher::new("Michel Foucault"),
    ];

    for p in &philosophers {
        p.eat();
    }
}

大した変更はありません。順に見ていきましょう。

use std::thread;

useは名前をスコープに持ち込みます。標準ライブラリからthreadモジュールを使いたいので、useが必要になります。

    fn eat(&self) {
        println!("{} is eating.", self.name);

        thread::sleep_ms(1000);

        println!("{} is done eating.", self.name);
    }

今回は間にsleep_ms()を挟んで、2つのメッセージを出力します。これで哲学者が食事をする時間をシミュレートしましょう。

このプログラムを実行すると、哲学者たちが順番に食事する様子がわかります:

Baruch Spinoza is eating.
Baruch Spinoza is done eating.
Gilles Deleuze is eating.
Gilles Deleuze is done eating.
Karl Marx is eating.
Karl Marx is done eating.
Friedrich Nietzsche is eating.
Friedrich Nietzsche is done eating.
Michel Foucault is eating.
Michel Foucault is done eating.

素晴らしい!ここまで来ました。残る問題はたった1つ: この問題の核心である並行性に関して、実際には手を付けていませんね!

哲学者たちを並行に食事させるには、小さな変更を加える必要があります。これが次のイテレーションです:

use std::thread;

struct Philosopher {
    name: String,
}   

impl Philosopher { 
    fn new(name: &str) -> Philosopher {
        Philosopher {
            name: name.to_string(),
        }
    }

    fn eat(&self) {
        println!("{} is eating.", self.name);

        thread::sleep_ms(1000);

        println!("{} is done eating.", self.name);
    }
}

fn main() {
    let philosophers = vec![
        Philosopher::new("Baruch Spinoza"),
        Philosopher::new("Gilles Deleuze"),
        Philosopher::new("Karl Marx"),
        Philosopher::new("Friedrich Nietzsche"),
        Philosopher::new("Michel Foucault"),
    ];

    let handles: Vec<_> = philosophers.into_iter().map(|p| {
        thread::spawn(move || {
            p.eat();
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }
}

main()内のループ変更と、1ヵ所の追加で全部です!まずは前半の変更から:

let handles: Vec<_> = philosophers.into_iter().map(|p| {
    thread::spawn(move || {
        p.eat();
    })
}).collect();

たったの5行ですが、うち4行は濃い内容となっています。では見ていきましょう。

let handles: Vec<_> = 

ここでは新しい束縛、handlesを導入します。今から新しいスレッドを作成していきますが、そのスレッドを制御するハンドル用にこの名前としました。後ほど議論する問題があるため、ここでは明示的な型アノテーションが必要となります。_は型プレースホルダ(type placeholder)です。つまり「handlesは何らかの型のベクトルとするが、その型が何であるかはRustが解決せよ。」と言っています。

philosophers.into_iter().map(|p| {

哲学者のリストに対してinto_iter()を呼び出します。このメソッドは、哲学者の所有権(ownership)を持つイテレータを生成します。スレッドに各要素を渡すため、このようにしました。イテレータに対してmapを呼び出し、その引数として要素毎に順番に呼ばれるクロージャ(closure)を渡します。

    thread::spawn(move || {
        p.eat();
    })

ここが並行実行される部分です。thread::spawn関数はクロージャを1つ引数にとり、新しいスレッド上でそのクロージャを実行します。このクロージャは特別なアノテーション、moveを必要とします。これによりキャプチャ(capturing)する値の所有権がクロージャ内へと移動されます。つまり、map関数の変数pが該当します。

スレッド内では、pに対してeat()を呼び出しておしまいです。

}).collect();

最後に、map呼び出しの結果をまとめ上げます。collect()は何らかのコレクション型を生成しますが、要求する戻り値型アノテーションを必要とします: ここではVec<T>です。またその要素はthread::spawn呼び出しの戻り値、つまり各スレッドへのハンドルとなっています。フゥー!

for h in handles {
    h.join().unwrap();
}

main()の最後に、ハンドルへのjoin()呼び出しをループし、各スレッド実行が完了するまで実行をブロックします。これにより、プログラム終了前にスレッド処理が完了すると保証します。

このプログラムを実行すると、哲学者たちが同不順に食事するさまが見られるでしょう!マルチスレッド処理だ!

Gilles Deleuze is eating.
Gilles Deleuze is done eating.
Friedrich Nietzsche is eating.
Friedrich Nietzsche is done eating.
Michel Foucault is eating.
Baruch Spinoza is eating.
Baruch Spinoza is done eating.
Karl Marx is eating.
Karl Marx is done eating.
Michel Foucault is done eating.

あれ、フォークはどこ行ったの?まだモデル化していませんでしたね。

という訳で、新しいstructを作っていきましょう:

use std::sync::Mutex;

struct Table {
    forks: Vec<Mutex<()>>,
}

このTableMutexのベクトルを保持します。ミューテックス(mutex)は並行処理を制御するための機構です: その内容へ同時アクセスできるのは1スレッドに限定されます。これは正に今回のフォークに求められる性質です。単に保持するだけで、実際に値を使うあても無いため、ミューテックスの中身は空タプル()とします。

プログラムでTableを使うよう変更しましょう:

use std::thread;
use std::sync::{Mutex, Arc};

struct Philosopher {
    name: String,
    left: usize,
    right: usize,
}

impl Philosopher {
    fn new(name: &str, left: usize, right: usize) -> Philosopher {
        Philosopher {
            name: name.to_string(),
            left: left,
            right: right,
        }
    }

    fn eat(&self, table: &Table) {
        let _left = table.forks[self.left].lock().unwrap();
        let _right = table.forks[self.right].lock().unwrap();

        println!("{} is eating.", self.name);

        thread::sleep_ms(1000);

        println!("{} is done eating.", self.name);
    }
}

struct Table {
    forks: Vec<Mutex<()>>,
}

fn main() {
    let table = Arc::new(Table { forks: vec![
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
    ]});

    let philosophers = vec![
        Philosopher::new("Baruch Spinoza", 0, 1),
        Philosopher::new("Gilles Deleuze", 1, 2),
        Philosopher::new("Karl Marx", 2, 3),
        Philosopher::new("Friedrich Nietzsche", 3, 4),
        Philosopher::new("Michel Foucault", 0, 4),
    ];

    let handles: Vec<_> = philosophers.into_iter().map(|p| {
        let table = table.clone();

        thread::spawn(move || {
            p.eat(&table);
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }
}

変更がたくさん!とはいえ、このイテレーションで、動作するプログラムが出来ました。細かく見ていきましょう:

use std::sync::{Mutex, Arc};

ここではstd::syncパッケージからもう一つの構造体: Arc<T>を利用します。その詳細は使うときに説明します。

struct Philosopher {
    name: String,
    left: usize,
    right: usize,
}

Philosopherに2つのフィールドを追加する必要があります。哲学者はそれぞれ2本のフォークを使います: 1本は左手に、もう1本は右手に。フォークの表現はベクトルのインデックスに対応するため、ここではusize型を使います。2つの値はTableが保持するforksのインデクス値を表しています。

fn new(name: &str, left: usize, right: usize) -> Philosopher {
    Philosopher {
        name: name.to_string(),
        left: left,
        right: right,
    }
}

インスタンス生成時にleftrightの値が必要になりますから、new()を拡張しました。

fn eat(&self, table: &Table) {
    let _left = table.forks[self.left].lock().unwrap();
    let _right = table.forks[self.right].lock().unwrap();

    println!("{} is eating.", self.name);

    thread::sleep_ms(1000);

    println!("{} is done eating.", self.name);
}

新しい行が2つあります。また新しい引数tableも追加しました。Tableが保持するフォークのリストにアクセスし、フォークにアクセスするためself.leftself.rightをインデクス値に用います。そのインデクスからMutexが得られたら、lock()を呼び出します。ミューテックスが別スレッドから並行アクセスされていた場合は、有効になるまでブロックされるでしょう。

lock()呼び出しは失敗する可能性があり、その場合は、プログラムをクラッシュさせます。この状況は、ミューテックスが'poisoned'状態、つまりロック保持中のスレッドがpanicした場合にしか発生しません。つまり今は起こりえないため、単にunwrap()を使っています。

もう一つの変わった点として: 結果を_left_rightと名づけました。このアンダースコアはなにもの?ええと、ロック内ではこれらの値を_使う_予定がありません。単にロックを獲得したいだけです。そうなると、Rustは値が未使用だと警告してくるでしょう。アンダースコアを使えば、Rustにこちらの意図を伝えることができ、警告されなくなるのです。

ロックの解放はどうしましょう?はい、_left_rightがスコープから抜けるとき、自動的に解放されます。

    let table = Arc::new(Table { forks: vec![
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
    ]});

続いて、main()では、新しいTableを作ってArc<T>に包んでいます。「arc」は「アトミック参照カウント(atomic reference count)」を意味し、複数スレッドからTableを共有するために必要となります。共有するときは参照カウントを増やし、各スレッドの終了時にはカウントを減らします。

let philosophers = vec![
    Philosopher::new("Baruch Spinoza", 0, 1),
    Philosopher::new("Gilles Deleuze", 1, 2),
    Philosopher::new("Karl Marx", 2, 3),
    Philosopher::new("Friedrich Nietzsche", 3, 4),
    Philosopher::new("Michel Foucault", 0, 4),
];

Philosopherのコンストラクタにはleftrightの値を渡す必要があります。ここではもう1つ細かい話がありますが、これは_非常に_重要な部分です。規則性という点では、最後以外は特に問題ありません。ムッシュ・フーコー(Foucault)は4, 0を引数にとるべきですが、代わりに、0, 4としています。これはデッドロックを防ぐためのものです。実は: 哲学者の一人は左利きだったのです!これは問題解決の一つのやり方ですが、私の見立てでは、最も単純な方法です。2

let handles: Vec<_> = philosophers.into_iter().map(|p| {
    let table = table.clone();

    thread::spawn(move || {
        p.eat(&table);
    })
}).collect();

最後に、map()/collect()ループの中で、table.clone()を呼び出します。Arc<T>clone()メソッドにより参照カウントが増加し、スコープ外に出たときは、カウントが減算されます。ここでは新しい束縛tableを導入して、古い方を覆い隠して(shadow)いることに気付くでしょう。これは2つの異なる名前を必要としないため、よく用いられる方法です。

これで、プログラムが動くようになりました!2人の哲学者だけが同時に食事ができるようになり、次のような出力がえられるでしょう:

Gilles Deleuze is eating.
Friedrich Nietzsche is eating.
Friedrich Nietzsche is done eating.
Gilles Deleuze is done eating.
Baruch Spinoza is eating.
Karl Marx is eating.
Baruch Spinoza is done eating.
Michel Foucault is eating.
Karl Marx is done eating.
Michel Foucault is done eating.

おめでとう!古典並行処理問題をRustを使って実装できましたね。


  1. 訳注:ソースコード中に登場する哲学者は、バールーフ・スピノザ(Baruch Spinoza)、ジル・ドゥルーズ(Gilles Deleuze)、カール・マルクス(Karl Marx)、フリードリヒ・ニーチェ(Friedrich Nietzsche)、ミシェル・フーコー(Michel Foucault)の5人。

  2. 訳注:5人目の哲学者に4, 0を与えると、プログラム全体がデッドロックする(可能性がある)。0, 4とすれば、ミューテックス・ロック優先順に全順序が定義でき、原理的にデッドロックが発生しなくなる。

21
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?