Rustでマルチスレッドプログラミングをやってみよう。
はじめに
空飛ぶ車って最近あるじゃないですか、あれは車なんですかね。 正直、人が乗れるドローンじゃないかと思うわけですよ。
なんかビジュアル見て既視感があるなとずっと思ってたんですが、起動戦士ガンダムのドダイですよあれ。
きっともう少し汎用的に広がりを見せ始めたら、ジャニーズみたいなアイドルのコンサートで、空飛ぶ車の上でパフォーマンス見れるようになると思ってんですが、安全的にはなしですかね。
今日のテーマ
マルチスレッドプログラミングをやっていきましょう。 下手な実装をすると、不具合の温床となることで有名なマルチスレッドです。 非常に有用なのですがやはり鬼門ですよね。順序とかを気にしないと行けないデータ受け渡しに使おうものなら もれなく地獄へのランデブーの始まりですが、Rustではプログラミング言語としてスレッドの安全性が担保されています。 安全じゃないパターンだとコンパイル時にエラーが出ることからそのように言われています。
スレッドを作って実行する
まずは、何も考えずにスレッドを作ってみます。
use std::thread;
fn main() {
thread::spawn(|| println!("thread test"));
}
実行結果
2パターンの実行結果が確認出来ます。
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target\debug\hello-learn.exe`
thread test
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target\debug\hello-learn.exe`
PS E:\SourceCode\Rust\hello-learn>
もうお気づきだとは思いますが、スレッドが呼び出されて実行されたパターンと スレッドが呼び出される前にプログラムが終了したパターンです。 このように、実行されたり、実行されなかったりと言うのがタイミングに依存するので一見ちゃんと動いてるように見えても 限られた条件下で動かしていると発生するような不具合に繋がりかねないのです。
私は、マルチスレッド素人なので、避けれるなら全力で避けたいなと思っています。 奥深い分野で、並行プログラミングとかはそれだけで1冊の本が書き上がるぐらいです。 私の興味の対象ではないのでサラッと書いていきますが…
スレッドの同期
スレッドが実行されてからプログラムを終了するようにします。
スレッドのハンドルを変数に格納して、join()を呼びスレッドが終了するのを待ち続けます。
use std::thread;
fn main() {
let handler = thread::spawn(|| println!("thread test"));
dbg!(handler.join());
}
実行結果は先程と変わらないものが得られるはずです。
スレッドをVecに詰め込む場合
例えば、画像を10分割して部分ごとに処理をしたい場合、並列作業したいようなケースがあると思います。 スレッドに分けて処理する際に、Vector型に処理ごとに詰め込むのが良いんじゃないかと思います。
fn main() {
let mut vec = Vec::new();
for x in 0..5 {
vec.push(std::thread::spawn(move || {
println!("Multi thread test::::{}", x);
}))
}
for elem in vec {
let _non = elem.join();
}
}
実行結果
動いてそうですね。
Compiling hello-learn v0.1.0 (E:\SourceCode\Rust\hello-learn)
Finished dev [unoptimized + debuginfo] target(s) in 0.86s
Running `target\debug\hello-learn.exe`
Multi thread test::::0
Multi thread test::::1
Multi thread test::::2
Multi thread test::::3
Multi thread test::::4
スレッド安全性
Rustの特徴に挙げられる機能で、スレッド安全性というものがあります。 スレッド間のデータ渡しを行ってその安全性を見ていきましょう。
データの共有
共有データに触る場合は、Mutexを使います。 OSなどでも使われているやつです。
Rustで一つのデータに対して複数のスレッドから同時にアクセスされないことを保証しています。 排他制御を行っていない場合、コンパイル時にエラーを出すことでスレッドの安全性を確保しているようです。 良く出来てるなぁ。
use std::sync::{Arc, Mutex};
fn main() {
let mut vec = Vec::new();
let data = Arc::new(Mutex::new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]));
for x in 0..10 {
let data_r = data.clone();
vec.push(std::thread::spawn(move || {
let mut data = data_r.lock().unwrap();
data[x] += 1;
println!("Thread No.{}", x)
}))
}
for elem in vec {
let _non = elem.join();
}
dbg!(data);
}
実行結果
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target\debug\hello-learn.exe`
Thread No.0
Thread No.1
Thread No.3
Thread No.9
Thread No.6
Thread No.5
Thread No.8
Thread No.7
Thread No.2
Thread No.4
[src\main.rs:20] data = Mutex {
data: [
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
],
poisoned: false,
..
}
まとめ
ガバガバ言語と違ってカチッとしてる。