本文へスキップ
BecomeCoder

Rustコース · 第10章 スマートポインタと並行性 · レッスン42

恐れないマルチスレッド ― 所有権が並行を安全にする

ローカル実施

導入

最後は、Rust がその設計の真価を発揮する 並行処理(マルチスレッド) です。並行処理は速い一方、データ競合という最恐のバグの温床でした。Rust はこれを——第5章で学んだ所有権と借用のルールを使って——コンパイル時に根絶します。だから「恐れないマルチスレッド(fearless concurrency)」と呼ばれます。メモリ核の学びが、ここで大きく実を結びます。

説明

スレッドを立てるには thread::spawn を使います。

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..5 {
            println!("スレッド: {i}");
        }
    });

    for i in 1..3 {
        println!("メイン: {i}");
    }

    handle.join().unwrap();   // スレッドの終了を待つ
}

ここで、第5章の借用ルールを思い出してください。「可変アクセスが1つでもあるなら、他からのアクセスは許さない」。このルールは、実はマルチスレッドでのデータ競合の条件そのものを禁じています。だから Rust では、スレッド間で安全に共有できないデータを共有しようとすると、コンパイルエラーになります

複数スレッドで1つのデータを共有・更新する正しい方法は、Arc(スレッド安全な Rc)と Mutex(一度に1スレッドだけアクセスを許す錠前)の組み合わせです。

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

fn main() {
    // Arc = スレッド安全な共有所有、Mutex = 排他制御つきの中身
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);   // 各スレッドへ所有権を共有
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();  // 錠前を取る(他は待つ)
            *num += 1;
        });   // スコープを抜けると錠前が自動で解放される
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("結果: {}", *counter.lock().unwrap());   // 10
}
  • Arc<T>Rc のスレッド安全版(Atomic Reference Counted)。カウントの増減がスレッド安全に行われます。シングルスレッドの Rc をここで使うと、コンパイラが「スレッド間で共有できない型だ」と拒否します。
  • Mutex<T> … 「同時に1スレッドしか中身に触れない」ことを保証する錠前。lock() で錠前を取り、他のスレッドはそれが解放されるまで待ちます。
  • Arc<Mutex<T>> … 「共有」+「排他制御」の、マルチスレッドの定番パターン。

決定的に重要なのは、この安全性がコンパイル時に検査されることです。Mutex で守らずに生のデータを複数スレッドで書き換えようとすれば、コンパイルが通りません。Rust は Send(スレッド間で所有権を移せる)と Sync(スレッド間で共有参照できる)というトレイトで「どの型がスレッドをまたいで安全か」を型システムに刻んでおり、危険な共有を機械的に弾きます。

graph TD
    rule["借用ルール:可変アクセス中は<br/>他のアクセスを許さない"]
    rule --> race["= データ競合の条件を満たせない"]
    race --> fearless["恐れないマルチスレッド<br/>データ競合はコンパイルを通らない"]
    tools["道具:Arc(共有)+ Mutex(排他)"] --> fearless

他の多くの言語では、データ競合は「運が悪いと本番でだけ再現する」悪夢のバグでした。Rust では、そもそもコンパイルが通らない。所有権という1つの仕組みが、メモリ安全(第3〜6章)だけでなく、並行安全まで一手に引き受けているのです。これこそが、Rust が「安全・速い・並行」を同時に名乗れる理由です。

試すには

上のコードで Arc::new(Mutex::new(0)) を単なる Mutex::new(0) にして複数スレッドで共有しようとすると、「所有権がムーブ済み」「スレッド間で共有できない」とコンパイラが拒否します。並行処理のバグを、実行前に机の上で潰せる——Rust の学びの総仕上げとして、その頼もしさを味わってください。