Rust でのマルチスレッドの勉強のためにぷよぷよっぽいゲームを書いているので、学んだことを書いておく。
Rust ではデータ競合は起こせない
Rust はデータ競合 ( data races ) がないことを保証している。( data races であって race conditions ではない。 Races - 参照
所有権のシステムによって同一のメモリを複数箇所から同時に書き換えることができないようになっており、これをコンパイル時にチェックすることで安全性を保証している。
これに違反している場合はコンパイルが通らない。
これはマルチスレッドでも適用されるので、安心してコードを書くことができるが、その分最初はコンパイルが通らなくて悩むことも多かった。
スレッドを使ってみる
単純な例
スレッドは標準ライブラリの thread::spawn
を使う。
以下はスレッドを生成して1秒後に hello と出力する。
use std::thread;
use std::time::Duration;
fn main() {
let t = thread::spawn(|| {
thread::sleep(Duration::from_secs(1));
println!("{}", "hello");
});
let _ = t.join();
}
https://is.gd/tnYGhZ
この場合はなんの問題もない。見た目通りの動作をしていると思う。
spawn
に無名関数 (|| {..}
は引数を取らないクロージャね) を渡していて、それが別スレッドで動く。
上のクロージャは外部の変数を使っていなかったが、変数を使うようにしてみる。
変数を出力するようにしてみる。
fn main() {
let name = "John";
let t = thread::spawn(|| {
thread::sleep(Duration::from_secs(1));
println!("hello {}", name);
});
let _ = t.join();
}
https://is.gd/deGnRU
これはエラーになる。
error[E0373]: closure may outlive the current function, but it borrows `name`, which is owned by the current function
--> <anon>:5:27
|
5 | let t = thread::spawn(|| {
| ^^ may outlive borrowed value `name`
6 | thread::sleep(Duration::from_secs(1));
7 | println!("hello {}", name);
| ---- `name` is borrowed here
|
help: to force the closure to take ownership of `name` (and any other referenced variables), use the `move` keyword, as shown:
| let t = thread::spawn(move || {
spawn
の signature を見てみると、引数は F: FnOnce() -> T, F: Send + 'static
とある。
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static
これはクロージャが 'static
な生存期間を持つことを意味している。
'static
な生存期間を持つクロージャが、生存期間が関数内だけの name
を借りようとしているのでクロージャのほうが長生きしてしまうよとエラーになる。
これはエラーで言われている通り、 move
キーワードを使ってクロージャに所有権を渡してしまえば良く、ほとんどのケースでは move
を使うことになる。
ただ、move
キーワードを使うということは当然所有権はクロージャ内に移ってしまうので、そのスレッド以外でも同じデータを使いたい場合には困ってしまう。
位置情報を持てる Enemy
構造体を作り、スレッド内で敵の位置を使ってなんらかの処理をしつつ、メインのスレッドで描画する(この場合は位置を表示するだけ)ことを考えてみる。
こうすると、enemy
の所有権が移った後に使おうとしているのでエラーになる
...
struct Enemy {
x: i32,
y: i32,
}
fn do_something(enemy: &Enemy) {
println!("{}", enemy.x + enemy.y);
}
fn main() {
let enemy = Enemy { x: 0, y: 0};
let _ = thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(1));
do_something(&enemy);
}
});
for _ in 0..5 {
thread::sleep(Duration::from_secs(1));
println!("({},{})", enemy.x, enemy.y);
}
}
https://is.gd/XJhXOZ
rustc 1.17.0 (56124baa9 2017-04-24)
error[E0382]: use of moved value: `enemy.x`
--> <anon>:24:29
|
15 | let _ = thread::spawn(move || {
| ------- value moved (into closure) here
...
24 | println!("({},{})", enemy.x, enemy.y);
| ^^^^^^^ value used here after move
|
そういった場合には、Arc
を使う。
Arc
は、 Atomic でスレッドセーフな参照カウンタ付きポインタで、スレッド間で安全に所有権を共有することを可能にする。
Arc
の中のデータはヒープ上にメモリが確保され、clone
すると中の値へのポインタが作られる。
clone
するたびに参照カウンタが増えていくが、それらの生存期間が終わるとカウンタは減り、0になるとデータが破棄される。
スレッド間での共有にはArc
を使う
Arc
を使うとこうなる。 Arc
で Enemy
を包んで、使う前には clone
する。
...
fn main() {
// Arcで包む
let enemy = Arc::new(Enemy { x: 0, y: 0});
{
// 使う時に clone する
let enemy = enemy.clone();
let _ = thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(1));
do_something(&enemy);
}
});
}
let enemy = enemy.clone();
for _ in 0..5 {
thread::sleep(Duration::from_secs(1));
println!("({},{})", enemy.x, enemy.y);
}
}
https://is.gd/e9clLQ
これはうまくいく。
もしあるスレッドでは敵を動かしたいんだという場合、これではうまくいかない。
&x
で参照を共有するときと同じように、Arc
で所有権を共有している場合データの競合を防ぐため、変更はできない。
ので、データを変更したい場合には更にもう1つ、 Mutex
が必要になる。
データの変更も行う場合はMutex
も使う
やろうと思ってもできないのはこんな感じ。敵を右に動かす
...
impl Enemy {
fn move_right(&mut self) {
self.x += 1;
}
}
fn main() {
let enemy = Arc::new(Enemy { x: 0, y: 0});
{
let mut enemy = enemy.clone();
let _ = thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(1));
// ここで敵を動かしたい
enemy.move_right();
}
});
}
....
}
これは、 clone
の時に mut
で変更可能な値として使おうとしているが、 immutable なのでできないよとエラーになる。
error: cannot borrow immutable borrowed content as mutable
--> <anon>:23:17
|
23 | enemy.move_right();
| ^^^^^ cannot borrow as mutable
Mutex
を使って排他制御をするとデータの競合を防げるので、データの変更ができるようになる。
Arc
の中のデータを Mutex
で包み、中のデータを使う際には lock
でロックを取得する。
取得したロックは、他のリソースと同様スコープを出ると自動で解放される。
ちなみに自分は lock
の型を見てロックが取れなければエラーを返すものだと思っていたが、それは try_lock
。
lock
の場合はロックが取れるまでブロックするので、他のスレッドがロックを解放してくれない場合はデッドロックする。
Mutex
を使うとこうなる。
fn main() {
// Mutexを追加
let enemy = Arc::new(Mutex::new(Enemy { x: 0, y: 0}));
{
let enemy = enemy.clone();
let _ = thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(1));
// 使う前にロックを取得する
let mut enemy = enemy.lock().unwrap();
enemy.move_right();
}
});
}
let enemy = enemy.clone();
for _ in 0..5 {
thread::sleep(Duration::from_secs(1));
// 変更はしないけどこちらもロックを取る必要はある
let enemy = enemy.lock().unwrap();
println!("({},{})", enemy.x, enemy.y);
}
}
https://is.gd/6DymJC
これだとxが変更されていくので出力はこうなる
(0,0)
(1,0)
(2,0)
(3,0)
(4,0)