Rustの恐れなき並行処理
RustのFearless Concurrencyをマスターする:スレッド生成、チャンネルによるメッセージパッシング、ArcとMutexによる状態共有、Send/Syncトレイト。
Rust中級シリーズの第15回。並行プログラミング(Concurrency)は、データ競合やデッドロック、メモリ破壊などが発生しやすく、伝統的に難易度が高い領域でした。
Rustはこれを Fearless Concurrency(恐れなき並行処理) という概念で解決します。所有権と型システムにより、並行処理におけるメモリ安全性の欠陥が実行時エラーではなく、コンパイル時エラーとして検出されます。
graph TD
A["Rustの恐れなき並行処理"] --> B["スレッド生成<br/>std::thread::spawn"]
A --> C["メッセージパッシング<br/>Channels (mpsc)"]
A --> D["状態の共有<br/>Arc<T> + Mutex<T>"]
A --> E["安全性の拡張<br/>Send & Sync トレイト"]
10歳でもわかる説明(ELI5):大忙しのレストランの厨房#
レストランの厨房をイメージしてください:
- シングルスレッド:1人のシェフが野菜切りから調理、皿洗いまで全て行います。安全ですがスピードに限界があります。
- 安全性のないマルチスレッド(C/C++):5人のシェフが何のルールもなく同じまな板やフライパンに手を伸ばし、誤って手を切ったり具材が混ざったりします(データ競合・メモリ破壊)。
- Rustの恐れなき並行処理:シェフごとに担当ブースが割り振られ(所有権)、注文伝票は専用レーン(チャンネル)で受け渡します。共有器具を使うときは鍵付きロッカー(Arc + Mutex)で管理されます。料理長(コンパイラ)が開店前に全手順を厳格にチェックします!
1. スレッドによるコードの並行実行#
Rustでは thread::spawn を使ってネイティブOSスレッドを生成します:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("子スレッド: {i}");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..=3 {
println!("メインスレッド: {i}");
thread::sleep(Duration::from_millis(1));
}
// 子スレッドの終了を待機
handle.join().unwrap();
}rustmove クロージャによる所有権の移動#
スレッド内で外側のスコープのデータを使用する場合、move キーワードで所有権をスレッドに移動(transfer)させます:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("スレッド内のベクタ: {v:?}");
});
handle.join().unwrap();
}rust2. メッセージパッシング:チャンネル (mpsc)#
Go言語で有名な格言「メモリを共有して通信するのではなく、通信することでメモリを共有せよ」は、Rustでも強く推奨されています。
標準ライブラリの mpsc(multiple producer, single consumer:複数の送信者、単一の受信者)を利用します:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone(); // 送信側をクローンして複数の送信スレッドを作成
// 送信スレッド 1
thread::spawn(move || {
let msgs = vec!["スレッド1", "からの", "メッセージ"];
for msg in msgs {
tx1.send(String::from(msg)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
// 送信スレッド 2
thread::spawn(move || {
let msgs = vec!["スレッド2", "からも", "送信"];
for msg in msgs {
tx.send(String::from(msg)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
// メインスレッドで受信
for received in rx {
println!("受信: {received}");
}
}rust3. 状態の共有:Arc<T> と Mutex<T>#
複数のスレッド間で同じデータを共有・変更する必要がある場合:
Mutex<T>(相互排他):同時に1つのスレッドのみがlock()を取得してデータにアクセスできるよう保護します。Arc<T>(アトミック参照カウント):複数のスレッド間で安全に所有権を共有できるスマートポインタです。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("最終カウント結果: {}", *counter.lock().unwrap()); // 10
}rust[!NOTE]
Rc<T>は参照カウントの更新がアトミックではないため、スレッドセーフではありません。スレッド間をまたぐデータ共有には、必ずArc<T>を使用してください。
4. 安全性を支える2つのトレイト:Send と Sync#
Rustの並行処理モデルは言語のコアに固定されているのではなく、以下の2つの自動トレイトに基づいています:
Send:スレッド間で型の所有権を転送できることを示します。Sync:複数のスレッドから型への参照(&T)を同時にアクセスしても安全であることを示します(T: Syncは&T: Sendと同等)。
Rustのほぼすべての基本型は自動的に Send かつ Sync です。スレッドセーフでない型(Rc<T> など)をスレッド間で共有しようとすると、コンパイラが即座にエラーを出して不正なプログラムの生成を防ぎます。
まとめ#
thread::spawnとmoveクロージャで安全なスレッド生成とデータ移動が行えます。mpsc::channelにより、所有権モデルを活かした安全なメッセージ通信が可能です。Arc<Mutex<T>>はマルチスレッド間で可変データを安全に共有するための標準パターンです。SendとSyncトレイトがコンパイル時のデータ競合防止を支えています。