Rust でマルチスレッドプログラミングのメモ

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 を使うとこうなる。 ArcEnemy を包んで、使う前には 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)