導入
最後は、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 の学びの総仕上げとして、その頼もしさを味わってください。