コードを同時に実行するためのスレッドの利用
現在のほとんどのオペレーティングシステムでは、実行されるプログラムのコードは プロセス 内で実行され、オペレーティングシステムは複数のプロセスを同時に 管理します。プログラムの内部では、同時に実行される独立した部分を持つことも できます。こうした独立した部分を実行する機能は、スレッド と呼ばれます。たと えば、Webサーバーは複数のスレッドを持つことで、同時に複数のリクエストに 応答できます。
プログラムの計算を複数のスレッドに分割して、複数のタスクを同時に実行する ことで、パフォーマンスが向上する場合がありますが、その一方で複雑さも増し ます。スレッドは同時に実行されうるため、異なるスレッド上のコードの各部分が どの順序で実行されるかについて、本質的な保証はありません。これにより、たと えば次のような問題が生じる可能性があります。
- 競合状態。スレッドがデータやリソースに不整合な順序で アクセスすること
- デッドロック。2つのスレッドが互いを待ち、どちらの スレッドも先に進めなくなること
- 特定の状況でしか発生せず、確実に再現して修正するのが難しい バグ
Rustはスレッド利用の悪影響を軽減しようとしていますが、それでも マルチスレッド環境でのプログラミングには慎重な検討が必要であり、 コード構造も単一スレッドで動くプログラムとは異なるものが求められます。
プログラミング言語はスレッドをいくつかの異なる方法で実装しており、多くの オペレーティングシステムは、新しいスレッドを作成するためにプログラミング 言語から呼び出せるAPIを提供しています。Rustの標準ライブラリは、スレッド 実装に 1:1 モデルを採用しており、このモデルでは、プログラムは言語レベルの 1つのスレッドにつき、オペレーティングシステムの1つのスレッドを使用します。 1:1モデルとは異なるトレードオフを行う、別のスレッドモデルを実装したcrateも あります。(次の章で見るRustのasyncシステムも、並行性に対する別の アプローチを提供します。)
spawn を使って新しいスレッドを作成する
新しいスレッドを作成するには、thread::spawn 関数を呼び出し、新しい
スレッドで実行したいコードを含むクロージャ(クロージャについては第13章で
説明しました)を渡します。リスト16-1の例では、メインスレッドからある
テキストを出力し、新しいスレッドから別のテキストを出力します。
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
Rustプログラムのメインスレッドが完了すると、spawnされたすべてのスレッド は、実行を完了しているかどうかにかかわらず停止されることに注意して ください。このプログラムの出力は毎回少し異なるかもしれませんが、次の ようなものになります。
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 の呼び出しは、スレッドに短時間その実行を停止させ、
別のスレッドが実行できるようにします。スレッドはおそらく交互に実行され
ますが、それは保証されていません。これは、オペレーティングシステムが
スレッドをどのようにスケジュールするかに依存します。この実行では、コード中
ではspawnされたスレッドの print 文のほうが先に現れているにもかかわらず、
メインスレッドが先に出力しました。また、spawnされたスレッドには i が 9
になるまで出力するよう指示しましたが、メインスレッドが終了する前に 5
までしか到達しませんでした。
このコードを実行して、メインスレッドからの出力しか見えない、またはまったく 重なりが見えない場合は、範囲内の数値を大きくして、オペレーティング システムがスレッド間を切り替える機会を増やしてみてください。
すべてのスレッドの終了を待つ
リスト16-1のコードでは、メインスレッドが終了するため、ほとんどの場合 spawnされたスレッドが途中で止められてしまうだけでなく、スレッドが実行 される順序にも保証がないため、spawnされたスレッドがそもそも実行される ことすら保証できません!
spawnされたスレッドが実行されない、または途中で終了してしまうという
問題は、thread::spawn の戻り値を変数に保存することで修正できます。
thread::spawn の戻り値の型は JoinHandle<T> です。JoinHandle<T> は
所有権を持つ値で、この値に対して join メソッドを呼び出すと、そのスレッド
が終了するまで待機します。リスト16-2は、リスト16-1で作成したスレッドの
JoinHandle<T> をどのように使うか、また、main が終了する前に
spawnされたスレッドが確実に完了するように join をどのように呼び出すか
を示しています。
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
ハンドルに対して 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() の呼び
出しによりメインスレッドは待機するため、spawnされたスレッドが完了する
まで終了しません。
では、代わりに main の for ループの前に handle.join() を移動
するとどうなるかを見てみましょう。
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
メインスレッドはspawnされたスレッドが終わるのを待ってから自分の for
ループを実行するので、ここに示すように、出力はもう入り混じらなくなります。
```text
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 を取り上げました。ここでは、
move と thread::spawn の相互作用により注目します。
リスト 16-1 では、thread::spawn に渡すクロージャが引数を取らないことに
注目してください。生成されたスレッドのコードでは、メインスレッドのデータを
何も使っていません。生成されたスレッドでメインスレッドのデータを使うには、
生成されたスレッドのクロージャが必要な値をキャプチャしなければなりません。
リスト 16-3 は、メインスレッドでベクタを作成し、それを生成された
スレッドで使おうとする試みを示しています。しかし、すぐにわかるように、
これはまだうまく動きません。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
このクロージャは 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
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'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
|
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 を
借用しようとします。しかし、ここには問題があります。Rust には、生成された
スレッドがどれくらい実行されるのか判断できないため、v への参照が常に
有効かどうかわからないのです。
リスト 16-4 は、v への参照が無効になる可能性がより高い状況を示しています。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
drop(v); // oh no!
handle.join().unwrap();
}
もし Rust がこのコードの実行を許したなら、生成されたスレッドがまったく実行
されないまま直ちにバックグラウンドに回される可能性があります。生成された
スレッドの内部には 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
|
6 | let handle = thread::spawn(move || {
| ++++
クロージャの前に move キーワードを追加すると、そのクロージャが使用している
値の所有権を取得するように強制でき、Rust にそれらの値を借用すべきだと
推論させずに済みます。リスト 16-5 に示した、リスト 16-3 への修正は、
意図どおりにコンパイルされて実行されます。
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();
}
メインスレッドが drop を呼び出していたリスト 16-4 のコードも、move
クロージャを使って同じように修正したくなるかもしれません。しかし、この修正は
機能しません。なぜなら、リスト 16-4 がやろうとしていることは、別の理由で
許可されていないからです。クロージャに move を追加すると、v はクロージャの
環境へムーブされるため、メインスレッドでそれに対して drop を呼び出すことは
もうできません。代わりに、このコンパイラエラーが出ます。
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `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
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
|
help: consider cloning the value before moving it into the closure
|
6 ~ let value = v.clone();
7 ~ let handle = thread::spawn(move || {
8 ~ println!("Here's a vector: {value:?}");
|
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 の
コードでエラーが出たのは、Rust が慎重に振る舞ってスレッドに対して v を
借用するだけにしていたためであり、その結果、理論上はメインスレッドが
生成されたスレッドの参照を無効にできてしまうからです。Rust に v の
所有権を生成されたスレッドへムーブするよう伝えることで、メインスレッドが
もはや v を使わないことを Rust に保証しています。リスト 16-4 も同じように
変更すると、メインスレッドで v を使おうとした時点で所有権ルールに
違反することになります。move キーワードは、借用するという Rust の慎重な
デフォルトを上書きするものであり、所有権ルールに違反できるようにする
ものではありません。
スレッドとは何かと、thread API が提供するメソッドを見てきたので、次は スレッドを使用できるいくつかの状況を見ていきましょう。