スレッドを使用してコードを同時に走らせる

多くの現代のOSでは、実行中のプログラムのコードはプロセスで走り、OSは同時に複数のプロセスを管理するでしょう。 プログラム内では、独立した部分を同時に実行することもできます。これらの独立した部分を走らせる機能をスレッドと呼びます。 例えばwebサーバは、同時に複数のリクエストに応答できるように、複数のスレッドを使用することができます。

同時に複数のタスクを実行するために、プログラム内の計算を複数のスレッドに分けることで、パフォーマンスを改善することができますが、 複雑度も増します。スレッドは同時に走らせることができるので、異なるスレッドのコードが走る順番に関して、 本来的に保証はありません。これは例えば以下のような問題を招きます:

  • スレッドがデータやリソースに矛盾した順番でアクセスする競合状態
  • 2つのスレッドがお互いを待ち、両者が継続するのを防ぐデッドロック
  • 特定の状況でのみ起き、確実な再現や修正が困難なバグ

Rustは、スレッドを使用する際の悪影響を軽減しようとしていますが、それでも、マルチスレッドの文脈でのプログラミングでは、 注意深い思考と、シングルスレッドで走るプログラムでのそれとは異なるコード構造が必要です。

プログラミング言語によってスレッドはいくつかの方法で実装されており、多くのOSは、言語が呼び出すことができる、 新規スレッドを生成するためのAPIを提供しています。 Rust標準ライブラリは1:1モデルのスレッド実装を使用しており、1つの言語スレッドに対して1つのOSスレッドを使用します。 1:1モデルとは異なるトレードオフを選択して、他のモデルのスレッドを実装するクレートもあります。

spawnで新規スレッドを生成する

新規スレッドを生成するには、thread::spawn関数を呼び出し、 新規スレッドで走らせたいコードを含むクロージャ(クロージャについては第13章で語りました)を渡します。 リスト16-1の例は、メインスレッドと新規スレッドからテキストを出力します:

ファイル名: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            //       "やあ!立ち上げたスレッドから数字{}だよ!"
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        //       "やあ!メインスレッドから数字{}だよ!"
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

リスト16-1: メインスレッドが別のものを出力する間に新規スレッドを生成して何かを出力する

Rustプログラムのメインスレッドが完了するときには、立ち上げられたすべてのスレッドは、その実行が完了したかどうかにかかわらず、 停止されることに注意してください。このプログラムからの出力は毎回少々異なる可能性がありますが、だいたい以下のような感じでしょう:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

thread::sleepを呼び出すと、少々の間、スレッドの実行を止め、違うスレッドを走らせることができます。 スレッドはおそらく切り替わるでしょうが、保証はありません: OSがスレッドのスケジュールを行う方法によります。 この実行では、コード上では立ち上げられたスレッドのprint文が先に現れているのに、メインスレッドが先に出力しています。また、 立ち上げたスレッドにはiが9になるまで出力するよう指示しているのに、メインスレッドが終了する前の5までしか到達していません。

このコードを実行してメインスレッドの出力しか目の当たりにできなかったり、オーバーラップがなければ、 範囲の値を増やしてOSがスレッド切り替えを行う機会を増やしてみてください。

joinハンドルで全スレッドの終了を待つ

リスト16-1のコードはほとんどの場合、メインスレッドが終了することで、立ち上げたスレッドが完了する前に停止されるだけでなく、 スレッドの実行順に保証がないことから、立ち上げたスレッドがそもそも実行されるかどうかも保証できません。

thread::spawnの戻り値を変数に保存することで、立ち上げたスレッドが実行されなかったり、 完了する前に終了してしまったりする問題を修正することができます。thread::spawnの戻り値の型はJoinHandleです。 JoinHandleは、そのjoinメソッドを呼び出したときにスレッドの終了を待つ所有された値です。 リスト16-2は、リスト16-1で生成したスレッドのJoinHandleを使用し、joinを呼び出して、 mainが終了する前に、立ち上げたスレッドが確実に完了する方法を示しています:

ファイル名: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

リスト16-2: thread::spawnJoinHandleを保存してスレッドが完了するのを保証する

ハンドルに対してjoinを呼び出すと、ハンドルが表すスレッドが終了するまで現在実行中のスレッドをブロックします。 スレッドをブロックするとは、そのスレッドが動いたり、終了したりすることを防ぐことです。 joinの呼び出しをメインスレッドのforループの後に配置したので、リスト16-2を実行すると、 以下のように出力されるはずです:

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!

2つのスレッドが代わる代わる実行されていますが、handle.join()呼び出しのためにメインスレッドは待機し、 立ち上げたスレッドが終了するまで終わりません。

ですが、代わりにhandle.join()forループの前に移動したらどうなるのか確認しましょう。こんな感じに:

ファイル名: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

メインスレッドは、立ち上げたスレッドが終了するまで待ち、それからforループを実行するので、 以下のように出力はもう混ざらないでしょう:

hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!

どこでjoinを呼ぶかといったほんの些細なことが、スレッドが同時に走るかどうかに影響することもあります。

スレッドでmoveクロージャを使用する

thread::spawnに渡されるクロージャでは、moveキーワードを多用することになるでしょう。 そうすることで、クロージャは環境から使用する値の所有権を奪い、あるスレッドから別のスレッドに値の所有権を移すからです。 第13章の「参照をキャプチャするか、所有権を移動するか」節では、クロージャの文脈でmoveについて議論しました。 それでは、movethread::spawnの相互作用により集中していきましょう。

リスト16-1において、thread::spawnに渡したクロージャには引数がなかったことに注目してください: 立ち上げたスレッドのコードでメインスレッドからのデータは何も使用していないのです。 立ち上げたスレッドでメインスレッドのデータを使用するには、立ち上げるスレッドのクロージャは、 必要な値をキャプチャしなければなりません。リスト16-3は、メインスレッドでベクタを生成し、 立ち上げたスレッドで使用する試みを示しています。しかしながら、すぐにわかるように、これはまだ動きません。

ファイル名: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        //       "こちらがベクタ: {:?}"
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

リスト16-3: 別のスレッドでメインスレッドが生成したベクタを使用しようとする

クロージャはvを使用しているので、vをキャプチャし、クロージャの環境の一部にしています。 thread::spawnはこのクロージャを新しいスレッドで走らせるので、 その新しいスレッド内でvにアクセスできるはずです。しかし、このコードをコンパイルすると、 以下のようなエラーが出ます:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
(エラー: クロージャは現在の関数よりも長生きするかもしれませんが、現在の関数が所有している`v`を借用しています)
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
  |                                  (借用されている値`v`より長生きするかもしれません)
7 |         println!("Here's a vector: {:?}", v);
  |                                           - `v` is borrowed here
  |                                            (`v`はここで借用されています)
  |
note: function requires argument type to outlive `'static`
(注釈: 関数は引数型が`'static`より長生きすることを要求しています)
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {:?}", v);
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
(ヘルプ: `v`(や他の参照されている変数)の所有権をクロージャに奪わせるには、`move`キーワードを使用してください)
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error

Rustはvのキャプチャ方法を推論し、println!vへの参照のみを必要とするので、クロージャは、 vを借用しようとします。ですが、問題があります: コンパイラには、立ち上げたスレッドがどのくらいの期間走るのかわからないので、 vへの参照が常に有効であるか把握できないのです。

リスト16-4は、vへの参照がより有効でなさそうな筋書きです:

ファイル名: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {:?}", v);
    });

    drop(v); // あちゃー!

    handle.join().unwrap();
}

リスト16-4: vをドロップするメインスレッドからvへの参照をキャプチャしようとするクロージャを伴うスレッド

このコードを実行できてしまうなら、立ち上げたスレッドはまったく実行されることなく即座にバックグラウンドに置かれる可能性があります。 立ち上げたスレッドは内部にvへの参照を保持していますが、メインスレッドは、第15章で議論したdrop関数を使用して、 即座にvをドロップしています。そして、立ち上げたスレッドが実行を開始する時には、vはもう有効ではなく、 参照も不正になるのです。あちゃー!

リスト16-3のコンパイルエラーを修正するには、エラーメッセージのアドバイスを活用できます:

help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
(ヘルプ: `v`(や他の参照されている変数)の所有権をクロージャに奪わせるには、`move`キーワードを使用してください)
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

クロージャの前にmoveキーワードを付することで、コンパイラに値を借用すべきと推論させるのではなく、 クロージャに使用している値の所有権を強制的に奪わせます。リスト16-5に示したリスト16-3に対する変更は、 コンパイルでき、意図通りに動きます:

ファイル名: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

リスト16-5: moveキーワードを使用してクロージャに使用している値の所有権を強制的に奪わせる

リスト16-4の、メインスレッドがdropを呼び出すコードを修正するためにも、 moveクロージャを使用して同じことを試したくなるかもしれません。しかしながら、 リスト16-4が試みていることは別の理由によりできないので、この修正はうまくいきません。 クロージャにmoveを付与したら、vをクロージャの環境にムーブするので、 最早メインスレッドでdropを呼び出すことは叶わなくなるでしょう。代わりにこのようなコンパイルエラーが出るでしょう:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
(エラー: ムーブされた値の使用: `v`)
  --> src/main.rs:10:10
   |
4  |     let v = vec![1, 2, 3];
   |         - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
   |          (`v`は`Copy`トレイトを実装しない`Vec<i32>`型を持つので、ムーブが発生します)
5  |
6  |     let handle = thread::spawn(move || {
   |                                ------- value moved into closure here
   |                                       (値はここでクロージャ内にムーブされます)
7  |         println!("Here's a vector: {:?}", v);
   |                                           - variable moved due to use in closure
   |                                            (変数はクロージャ内で使用されているためムーブされます)
...
10 |     drop(v); // oh no!
   |          ^ value used here after move
   |           (値はここでムーブ後に使用されています)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error

再三Rustの所有権規則が救ってくれました!リスト16-3のコードはエラーになりました。 コンパイラが一時的に保守的になり、スレッドに対してvを借用しただけだったからで、 これは、メインスレッドは理論上、立ち上げたスレッドの参照を不正化する可能性があることを意味します。 vの所有権を立ち上げたスレッドに移動するとコンパイラに指示することで、 メインスレッドはもうvを使用しないとコンパイラに保証しているのです。リスト16-4も同様に変更したら、 メインスレッドでvを使用しようとする際に所有権の規則に違反することになります。 moveキーワードにより、Rustの保守的な借用のデフォルトが上書きされるのです; 所有権の規則を侵害させてくれないのです。

スレッドとスレッドAPIの基礎知識を得たので、スレッドでできることを見ていきましょう。