はじめに
Rustは「データ競合をコンパイル時に防ぐ」ことを言語レベルで保証している珍しい言語です。その鍵となるのが Arc と Mutex の組み合わせです。
本記事では、マルチスレッドプログラムで複数のスレッドから同じデータを安全に読み書きする方法を、基礎から丁寧に解説します。
なぜ共有状態は難しいのか
まず、スレッド間でデータを共有しようとすると何が起きるかを確認しましょう。
use std::thread;
fn main() {
let mut counter = 0;
let handle = thread::spawn(|| {
counter += 1; // コンパイルエラー!
});
handle.join().unwrap();
println!("counter: {}", counter);
}
上記のコードはコンパイルエラーになります。Rustの所有権システムが「counterの所有権がメインスレッドにあるのに、別スレッドから借用しようとしている」と検出するためです。
では、どうすれば安全に共有できるのでしょうか?
Arc:複数スレッドで所有権を共有する
Arc(Atomically Reference Counted)は、複数のスレッドで安全に値を共有するためのスマートポインタです。通常のRcはスレッドをまたいで使えませんが、Arcは内部の参照カウントをアトミック操作で管理するためスレッドセーフです。
use std::sync::Arc;
use std::thread;
fn main() {
let value = Arc::new(42);
let value_clone = Arc::clone(&value);
let handle = thread::spawn(move || {
println!("スレッド内の値: {}", value_clone);
});
handle.join().unwrap();
println!("メインの値: {}", value);
}
Arc::cloneはポインタのコピーだけを行い、中のデータはコピーしません。どちらのクローンも同じデータを指しています。
ただし、Arcだけでは読み取り専用です。値を書き換えるにはMutexと組み合わせる必要があります。
Mutex:排他制御でデータを保護する
Mutex(Mutual Exclusion)は、一度に一つのスレッドだけがデータにアクセスできるようにするための仕組みです。
use std::sync::Mutex;
fn main() {
let m = Mutex::new(0);
{
let mut val = m.lock().unwrap();
*val += 1;
} // ここでロックが自動的に解放される
println!("値: {:?}", m.lock().unwrap());
}
lockメソッドの仕組み
m.lock()を呼ぶと:
- 他のスレッドがロックを持っていれば、解放されるまでブロックして待機する
- ロックを取得できたら
MutexGuardという型の値が返る MutexGuardがスコープを抜けると(Dropが呼ばれると)ロックが自動解放される
ロックの解放忘れが構造上起こりにくい設計になっています。
ArcとMutexを組み合わせる
実際のマルチスレッドプログラムでは、Arc<Mutex<T>>という組み合わせがよく使われます。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc<Mutex<i32>> を作成
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
// 各スレッドにクローンを渡す
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
println!("スレッド内カウンター: {}", *num);
});
handles.push(handle);
}
// 全スレッドの終了を待つ
for handle in handles {
handle.join().unwrap();
}
println!("最終カウンター: {}", *counter.lock().unwrap());
}
実行結果(順序は実行ごとに異なります):
スレッド内カウンター: 1
スレッド内カウンター: 2
スレッド内カウンター: 3
スレッド内カウンター: 4
スレッド内カウンター: 5
最終カウンター: 5
5つのスレッドが互いに干渉することなく、安全にカウンターをインクリメントできています。
デッドロックに気をつける
Mutexを使う際に注意すべきなのがデッドロックです。同じスレッドが同じロックを二重に取得しようとするとハングします。
use std::sync::Mutex;
fn main() {
let m = Mutex::new(0);
let _lock1 = m.lock().unwrap();
let _lock2 = m.lock().unwrap(); // ここで永遠に待ち続ける(デッドロック)
}
デッドロックを防ぐには:
- ロックを持つ時間をできるだけ短くする(スコープを絞る)
- 複数のMutexを使う場合は常に同じ順番でロックを取得する
- 必要に応じて
try_lock()でブロックせずに試みる
まとめ
| 型 | 役割 |
|---|---|
Arc<T> | 複数スレッド間で所有権を共有する |
Mutex<T> | 排他制御でデータへのアクセスを保護する |
Arc<Mutex<T>> | 複数スレッドから安全に読み書きする |
Rustでは「コンパイルが通ればデータ競合はない」という安心感があります。ArcとMutexを正しく組み合わせることで、複雑なマルチスレッドプログラムも安全に実装できます。
次のステップとして、RwLock(複数読み取り・単一書き込みを許可するロック)や、前回紹介したmpscチャンネルとの使い分けも学んでみましょう。