TL;DR

  • 借用ルールにより、可変な値をそのまま複数のスレッドで共有しようとするとコンパイルエラーになる。
  • Arc<Mutex<T>> を使うと安全に共有できる。unsafe を使わない安全な Rust では、データ競合を完全に避けられる。

内容

項目17 状態共有並列実行には気を付けようのうち、データ競合についての話である。

借用ルールが共有を止める

可変参照は同じスコープに 1 つしか作れないので、次の構造体のインスタンスを複数のスレッドで共有しようとするとコンパイルエラーになる。

pub struct BankAccount {
    balance: i64,
}
let mut account = BankAccount::new();
let _payer = std::thread::spawn(move || pay_in(&mut account));
let _taker = std::thread::spawn(move || take_out(&mut account));

Mutex で包む

コンパイルを通すには、同期プリミティブの Mutex を使う。C++ の std::mutex などとは違い、Rust の Mutex は独立したオブジェクトではなく、守りたいデータを包むラッパである。

pub struct BankAccount {
    balance: std::sync::Mutex<i64>,
}

ロックを取る Mutex::lock は Result<MutexGuard<T>, PoisonError<MutexGuard<T>>> を返す。MutexGuard は RAII パターンになっていて、スコープの終わりでドロップされるとロックが解除される(項目11)。

戻り値が Result なのは、ロックが poisoning の状態になることがあるからである。poisoning とは、ロックを持ったスレッドが panic! を起こし、ロックが正常に解放されなかった状態を指す。ただし、めったに起きないうえ、起きたら即座にプログラムを止めたほうがよい場合が多いので、unwrap() で済ませることが多い。

MutexGuard は Deref と DerefMut を実装しているので、中の値へのプロキシとしても使える。

pub fn balance(&self) -> i64 {
    let balance = *self.balance.lock().unwrap();
    if balance < 0 {
        panic!("** Oh no, gone overdrawn: {}", balance);
    }
    balance
}
 
pub fn deposit(&self, amount: i64) {
    *self.balance.lock().unwrap() += amount
}
 
pub fn withdraw(&self, amount: i64) -> bool {
    let mut balance = self.balance.lock().unwrap();
    if *balance < amount {
        return false;
    }
    *balance -= amount;
    true
}

借用ルールでは複数の参照を可変にできないので、balance を変更するのに、メソッドは &mut self ではなく &self を受け取っている。これは内部可変性の一例である(項目8)。借用のチェックは事実上コンパイル時から実行時に移り、代わりにスレッド間の同期の仕組みが入る。

Arc で所有を共有する

move によって account が 1 つ目のクロージャにムーブし、そのクロージャが終わるとドロップされるので、balance を Mutex で包んでも、今度は生存期間のエラーが出る。

let mut account = BankAccount::new();
let _payer = std::thread::spawn(move || pay_in(&account));
let _taker = std::thread::spawn(move || take_out(&account));

これを避けるには std::sync::Arc を使う。

let account = std::sync::Arc::new(BankAccount::new());
account.deposit(1000);
 
let account2 = account.clone();
let _taker = std::thread::spawn(move || take_out(&account2));
 
let account3 = account.clone();
let _payer = std::thread::spawn(move || pay_in(&account3));

各スレッドは、参照カウント付きポインタのコピーをクロージャで受け取る。参照されている BankAccount は、参照カウントが 0 になったときにだけドロップされる。

以上から、unsafe のコードを除く安全な Rust では、データ競合を完全に避けられる。

キーワード