Rustのスレッド安全


目的

スレッド安全をどのように実現しているか知る

まとめ

①スレッド間でデータがそもそも共有されないようにする。
②スレッド間で共有される場合は同じ領域に同じタイミングでアクセスすることがないかをコンパイル時にチェックしてくれる。

スレッド

thread::spawn(|arg|{body});で使用する。

qiita.rs
use std::thread;

fn main() {

  let handle = thread::spawn(|| {
    println!("1", );
  });

  println!("2", );

}

実行結果

10個のスレッドの実行。どの順番で実行されるかはわからない。

qiita.rs
use std::thread;
fn main() {

  let mut handles=Vec::new();
  for x in 0..10{
    handles.push(thread::spawn(move || {
      println!("{}", x);//moveさせないと参照のスコープから外れてしまう。
    }))
  }

  for handle in handles{
    let _=handle.join();//joinは実行完了まで
  }

}

スレッド間でのデータの共有

スレッド間でデータを共有させない

dataというvectorの値をスレッド毎に+1する処理。
以下は、スレッド間でデータを共有する場合に安全でなければ、コンパイルエラーになる例。

qiita.rs
use std::thread;

fn main() {

  let mut data=[0,1];//共有データ
  let mut handles=Vec::new();
  for x in 0..2{
    handles.push(thread::spawn(move || {
      data[x]+=1;
      println!("{}", x);
    }))
  }

  for handle in handles{
    let _=handle.join();//joinは実行完了まで
  }

}

以下のように、1個目のスレッドが実行された際にdataの所有権が1つ目のスレッドに移行してしまい。他のスレッドで使用できなくなる。

所有権を共有する仕組み

所有権を共有するために、Rcという参照カウンタ式のスマートポインタがある。
C++のshared_ptr同様参照カウンタが0になると自動的に解放される。
デフォルトは不変。

qiita.rs
use std::rc::Rc;

fn main() {

  let data=Rc::new([0,1]);
  println!("参照数 {}", Rc::strong_count(&data));//1
  {
    let data2=data.clone();//参照カウントUP
    println!("参照数 {}", Rc::strong_count(&data));//2
    for x in 0..2{
      println!("{}", data2[x]);
    }

  }//解放

  println!("参照数 {}", Rc::strong_count(&data));//1

}

Rcを使ってデータを共有してみる

qiita.rs
use std::rc::Rc;
use std::thread;

fn main() {
  let mut handles=Vec::new();
  let mut data=Rc::new([0,1]);//共有データ

  for x in 0..2{
    let mut ref_data=data.clone();//参照カウントUP

    handles.push(thread::spawn(move || {
      ref_data[x]+=1;
      println!("{}", ref_data[x]);
    }))
  }

  for handle in handles{
    let _=handle.join();//joinは実行完了まで
  }

}

データをスレッド間では安全に渡せないとエラーになる。Rcはマルチスレッドでは使えない。
マルチスレッドの時に別のスレッドに妨害されず正しく参照をカウントアップしたり、カウントダウンできることを確認できていないためらしい。

マルチスレッドでも使用可能なスマートポインタArc(Automatically Reference Counted).

RcをArcに変えてコンパイル。

qiita.rs
use std::sync::Arc;
use std::thread;

fn main() {
  let mut handles=Vec::new();
  let mut data=Arc::new([0,1]);//共有データ

  for x in 0..2{
    let mut ref_data=data.clone();//参照カウントUP

    handles.push(thread::spawn(move || {
      ref_data[x]+=1;
      println!("{}", ref_data[x]);
    }))
  }

  for handle in handles{
    let _=handle.join();//joinは実行完了まで
  }

}

Arcは書き換えできないと怒られる。
ref_data[x/2]+=1の時、スレッド0と1では同じ領域にアクセスして競合が起こる可能性があるためエラーになっている。

Mutexを使用して1つのスレッドからデータのアクセスを許可する

lock()を使用するとデータへのロックと解除が自動で行われ、競合が起こらないことが保証される。

qiita.rs

use std::sync::{Arc,Mutex};
use std::thread;

fn main() {
  let mut handles=Vec::new();
  let mut data=Arc::new(Mutex::new([0,1]));//共有データ

  for x in 0..2{
    let ref_data=data.clone();//参照カウントUP

    handles.push(thread::spawn(move || {
      let mut data=ref_data.lock().unwrap();
      data[x]+=1;
      println!("{}", data[x]);
    }))
  }

  for handle in handles{
    let _=handle.join();//joinは実行完了まで
  }

}

実行結果