Rustの勉強(その7)
チュートリアルはこれで最後であります。
mao-instantlife.hatenablog.com
前回の続き。
チュートリアルのソース全体像
やってる間にドキュメントの構成が変わったようで、食事する哲学者の問題とかRust Inside Other Languageとかがなくなってます。というわけで、サンプルの全体像を先に出しておきます。一応、今は亡きチュートリアルに従って動作確認済み。
use std::thread; use std::time::Duration; use std::sync::{Mutex, Arc}; // 余談:use句は複数のものを `{}` で省略して書くことが可能 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(); thread::sleep(Duration::from_millis(150)); let _right = table.forks[self.right].lock().unwrap(); println!("{} is eating.", self.name); thread::sleep(Duration::from_millis(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 philosopers = vec![ Philosopher::new("Judith Butler", 0, 1), Philosopher::new("Gilles Deleuze", 1, 2), Philosopher::new("Karl Marx", 2, 3), Philosopher::new("Emma Goldman", 3, 4), Philosopher::new("Michel Foucault", 4, 0), ]; let handles: Vec<_> = philosopers.into_iter().map(|p| { let table = table.clone(); thread::spawn(move || { p.eat(&table); }) }).collect(); for h in handles { h.join().unwrap(); } }
Mutex
テーブルを実装します。フォークは同時に一人しか手に取ることができない、かつテーブルにおける数も決まっているので、排他制御を行います。
struct Table { forks: Vec<Mutex<()>>, }
Mutexという並列実行時に排他制御を行うためのオブジェクトで定義をしております。型引数を空タプルに指定。すみません、セマフォしか知らなかったorz
Mutex利用の準備
struct Philosopher { name: String, left: usize, right: usize, }
哲学者に右手と左手を実装って書くと人体練成してる気分になりますが、右手と左手がどのフォークを取るのか、というインデックスで表しています。理解が浅かったのでついでに調べましたが、 usize
はポインタと同じサイズの符号なし整数です。ドキュメントにサイズが書いてなかったのですが、実行環境依存であってるんですかね?同様に eat
メソッドも以下のように変更して「フォークを持って食べる」というシーケンスを実装しています。
fn eat(&self, table: &Table) { let _left = table.forks[self.left].lock().unwrap(); thread::sleep(Duration::from_millis(150)); let _right = table.forks[self.right].lock().unwrap(); println!("{} is eating.", self.name); thread::sleep(Duration::from_millis(1000)); println!("{} is done eating.", self.name); }
Mutexの挙動
Mutexは、先に誰か掴まれていた場合、ロック取得可能になるまでブロックします。ただ、ロック開放待ちならともかく、ロック取得に失敗した時にはクラッシュさせたいですね。Rustは poisoning
と呼ぶ戦略でmutexを管理していて、
もしロック取得時にパニックが起こったら他のスレッドはMutexにアクセスできなくなるという仕組みのようです。そのため、コード上ではハンドリング不要の模様。パニックが起こらなかったら unwrap
します。で、ロックの解放は、 _left
と _right
がブロックからはずれたら、つまりこのケースだと eat
メソッドが終了したら、自動で解放されます。
ちょっと余談ですが、ロックの取得を受ける変数の頭にアンダースコアをつけているのは未使用による警告防止のため。Rustは、ブロック内に未使用の変数がある場合は警告するが、変数名の頭にアンダースコアがあると警告を出さないようにしてくれます(実際試したけど、標準出力のコピーとるの忘れた)。
哲学者の食事とデッドロックの回避
let philosopers = vec![ Philosopher::new("Judith Butler", 0, 1), Philosopher::new("Gilles Deleuze", 1, 2), Philosopher::new("Karl Marx", 2, 3), Philosopher::new("Emma Goldman", 3, 4), Philosopher::new("Michel Foucault", 4, 0), ];
実際に哲学者に食事をさせるために「どのフォークを取るか」を錬成時に指定します。ある哲学者の右手とその隣の哲学者の左手のインデックスが重なってループしないように指定します。この時、Michel Faucaultさんのインデックスが(4,0)だったら、右手が必ず解放待ちになるためにデッドロックが起こります。
Arc(Atomic Reference Counting)
で、ここはまだちゃんと理解していないところ。同時に1スレッドしか参照されないオブジェクトのカウントをしている?Javaで言うsynchronizedみたいなものだと理解してるんだけど、違うかな?
let table = Arc::new(Table{ forks: vec![
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
]});
で、その参照カウントですが、 table.clone()
でインクリメントしてブロックから出る時にデクリメントしていると説明があります。ちょっと雑な読み方してるので間違ってるかもしれませんが、本来ならtableに参照カウントのインクリメントやデクリメントを通知する必要があるが、tableのシャドウを作ることとブロックを外れることで通知している、との説明。
let handles: Vec<_> = philosopers.into_iter().map(|p| { let table = table.clone(); thread::spawn(move || { p.eat(&table); }) }).collect();
というわけで
チュートリアルはおしまいです。次からは、シンタックスなどの章に入ります。BitBarと合わせて遊んでみようかな、とか考え中。