冥冥乃志

ソフトウェア開発会社でチームマネージャをしているエンジニアの雑記。アウトプットは少なめです。

follow us in feedly

Rustの勉強(その6)

前回の続きです。ところでタブ固定してたんで気付かなかったんですが、この食事する哲学者の問題のチュートリアル、公式のドキュメントにリンクが見当たらないようになってるんですけど、どういうことでしょうね?

mao-instantlife.hatenablog.com

expression based

なんと訳せばいいのかよく分からないんですが、全ては式として定義されて値がリターンされますよ、で、指定がなければ最後の式がリターンされますよ、というScalaとかでもおなじみの挙動のことのようです。

インスタンスの生成(newを使わない場合)

let px = Philosopher { name = "takaaki yoshimoto".to_string() };

でもいけるっぽいですね。new使う方がすっきりしてるので(今回は)そっちでいきます。

配列の定義

let philosopers = vec![
    Philosopher::new("Judith Butler"),
    Philosopher::new("Gilles Deleuze"),
    Philosopher::new("Karl Marx"),
    Philosopher::new("Emma Goldman"),
    Philosopher::new("Michel Foucault"),
];

動的配列 Vec<T>' を作っています。特にチュートリアルに記載がないですが、今までの記法からしてvec!` は動的配列を生成するためのマクロなんでしょうかね。

配列のイテレーションには for を使います。

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

よく見る記法です。

インスタンスメソッド(method)の追加

Philosopher 型にメソッドを追加します。

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

associated functionの定義 -> selfパラメータなし methodの定義 -> 明示的にselfパラメータをつける

並列処理

哲学者どもに並列で食事をしてもらいます。

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

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

最初の map の実行で配列 philosophers の各要素に対して eat メソッドを実行するスレッドのハンドルの配列を作ります。ハンドルはスレッドをメインプロセスからコントロールするための値。結果を受け取ったりなんかに使います。むかーしWin32ネイティブアプリを作ってた以来ですね、ハンドルって単語を使ったの。配列の型はプレイスホルダーで指定。いつものことながら、細かいことは後でね♪という感じ。「handlesは何かの配列なんだけど、Rustは何の配列かちゃんとわかってるからね」とのこと。その後のループで各スレッドの終了をハンドリングしています。

ちなみに map メソッドの引数はクロージャです。クロージャ|引数| {処理} の形式で指定します。

into_iter メソッドは、配列のメソッドイテレーションを返すみたい。何で一々イテレータを取得?とか思ったら、mapは core::iter::Iterator 型のメソッドなんですね。

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

試しに上記のように配列で直接 map するコードにしてビルドしてみたら、以下のようなエラーが出ました。

src/main.rs:39:17: 43:7 note: the method `map` exists but the following trait bounds were not satisfied: `collections::vec::Vec<Philosopher> : core::iter::Iterator`, `[Philosopher] : core::iter::Iterator`

collectイテレータを配列に変換しています。 map の戻り値はイテレータなんですね。この辺りの違いをちゃんと押さえてない情弱でしたorz ちなみにこれ、わざわざ collect しないで走らせると(ビルドはちゃんと通る)並列処理になってない気がするんだけど、どうしてでしょう?

スレッドを作る

で、いよいよ本題。サブスレッドを作るには thread::spawn 関数 *1 を使います。

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

spawn の引数はスレッドで実行される処理を指定したクロージャです。 spawn とは卵を産むとかそういう意味なんですね。メインプロセスがスレッドを産む、というイメージでしょうか。

クロージャに指定している move は、map が受け取ったクロージャの引数 p の所有権を取得するためのアノテーションです。というか || をorの意味だと思ってたんですが、引数リストが空という意味なんですね。プライベートな変数は完全に自分のブロック内だけで有効で、子の関係にあるブロックでも引き継がれないようです。一瞬面倒だと思ったけど、わかりやすくなりますね。ちなみに、 move アノテーションを外してビルドすると以下のようなエラーが出ます。

src/main.rs:35:13: 35:14 note: `p` is borrowed here
src/main.rs:35             p.eat();
                           ^
src/main.rs:34:24: 36:10 help: to force the closure to take ownership of `p` (and any other referenced variables), use the `move` keyword, as shown:
src/main.rs:           thread::spawn( move || {
src/main.rs:               p.eat();
src/main.rs:           }) 

別の時にも思いましたけど、エラーメッセージがかなり親切な気がしますね。「これじゃね?」というサジェストをしてくれるのはなんかありがたいです。

ちなみに、 thread.spawn にはセミコロンがありません。正しい値を移譲するため、と書いてあります。例によって別章で詳細です。

スレッドの完了を保証する

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

thread.spawn で作成したスレッドの結果(Result型)を join メソッドで受け取ることができます。この結果から値を取り出しているのが unwrap です。

ja.stackoverflow.com

そろそろ英語が辛くなってきてますが

以下次号です。

*1:associated functionは関数と言ってしまって問題ない模様。チュートリアルでassociatedを省略してるし