Arc/Mutexによる状態共有
安全な共有データ
「Arc/Mutexによる状態共有」はCoddyKit上の無料Learn Rust Codingレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLearn Rust Coding学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Learn Rust Codingコースには全4レッスンが含まれています。
共有状態が難しい理由
メッセージパッシングだけでは不十分で、複数のスレッドが同じデータを読み書きする必要がある場合もあります。Rustでは、保護なしに可変値をスレッド間で共有できません。それを許すとデータ競合が起こる危険があるためです。
組み合わせて使うツールは次の2つです。
Arc:スレッド間で共有所有権を持つために使います。Mutex:安全に排他的な変更を行うために使います。
Rcはスレッドセーフではない
Rcは共有所有権を提供しますが、1つのスレッド内でしか使えません。参照カウントが同期されないため、コンパイラーはスレッド間での受け渡しを拒否します。マルチスレッドで共有するにはArc(Atomically Reference Counted:アトミック参照カウント)が必要です。
ArcはRcと同じように動作しますが、アトミック操作でカウントを更新するため、スレッド間で安全にcloneできます。
use std::sync::Arc;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let clone1 = Arc::clone(&data);
println!("original: {:?}", data);
println!("clone: {:?}", clone1);
println!("count: {}", Arc::strong_count(&data));
}Mutexによる排他的アクセス
Mutexはデータを包み、一度に1つのスレッドだけがアクセスできることを保証します。.lock()を呼び出すとMutexGuardを取得でき、これは内部の値をデリファレンスします。ガードがスコープを抜けると、ロックは自動的に解放されます。
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num += 10;
} // guard dropped here, lock released
println!("value = {:?}", m.lock().unwrap());
}ArcとMutexの組み合わせ
スレッド間で可変データを共有するには、Arcで包みます。
Arcにより、多数のスレッドが同じデータへのハンドルを所有できます。Mutexにより、各スレッドが一度に1つずつ安全にデータを変更できます。
スレッドを生成する前に、各スレッド用にArcをcloneします。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
});
handle.join().unwrap();
println!("counter = {}", *counter.lock().unwrap());
}複数スレッドで共有するカウンター
典型的な例は、10個のスレッドが共有カウンターをそれぞれインクリメントする処理です。各スレッドは独自のArcのcloneを持ち、Mutexをロックして1を加算します。すべてのスレッドをjoinすると、データ競合なしに合計は正確に10になります。
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 c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}ロックのスコープが重要
MutexGuardはdropされるまでロックを保持します。時間のかかる処理中も保持していると、ほかのスレッドをブロックします。クリティカルセクションは短く保ちましょう。ロックし、変更し、解放します。ロックを小さなブロックで囲むと、追加の処理を行う前に確実にdropされます。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut handles = vec![];
for i in 0..3 {
let l = Arc::clone(&log);
handles.push(thread::spawn(move || {
{
let mut v = l.lock().unwrap();
v.push(i);
} // released quickly
}));
}
for h in handles { h.join().unwrap(); }
let mut result = log.lock().unwrap().clone();
result.sort();
println!("{:?}", result);
}デッドロックという現実的な危険
デッドロックとは、2つのスレッドが互いに相手の必要とするロックを保持し、両方が永遠に待ち続ける状態です。Rustはデータ競合を防ぎますが、デッドロックまでは防ぎません。複数のmutexを常に同じ順序でロックし、ロックの保持時間を短くすることで回避してください。
また、1つのスレッドで同じMutexを2回ロックすることも避けてください。標準のMutexは再入可能ではありません。
スレッドがパニックしたときのpoisoning
ロックを保持しているスレッドがパニックすると、Mutexはpoisoned状態になります。その後の.lock()呼び出しはErrを返すため、データが不整合な可能性を把握できます。データがまだ使用可能だと判断した場合は、エラーに対してinto_inner()を呼び出して内部のガードを復元できます。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(0));
let d = Arc::clone(&data);
let _ = thread::spawn(move || {
let mut g = d.lock().unwrap();
*g = 7;
panic!("boom"); // poisons the mutex
}).join();
match data.lock() {
Ok(g) => println!("ok: {}", *g),
Err(poisoned) => println!("recovered: {}", *poisoned.into_inner()),
}
}多数の読み取りにはRwLock
書き込みよりも読み取りが圧倒的に多い場合、RwLockはMutexより高速になることがあります。多数の同時読み取りまたは1つの書き込みを許可します。共有アクセスには.read()を、排他的アクセスには.write()を使います。
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let config = Arc::new(RwLock::new(String::from("v1")));
let reader = Arc::clone(&config);
let r = thread::spawn(move || {
let val = reader.read().unwrap();
println!("read: {}", *val);
});
r.join().unwrap();
{
let mut w = config.write().unwrap();
*w = String::from("v2");
}
println!("final: {}", *config.read().unwrap());
}単純なカウンターにはアトミック型
単一の整数カウンターに完全なMutexを使うのは過剰です。AtomicUsizeなどの型は、fetch_addのようなメソッドによってロックなしで更新できます。スレッド間で共有するにはArcで包みます。Orderingを選択してください。最も単純で安全なデフォルトはSeqCstです。
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..5 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles { h.join().unwrap(); }
println!("count = {}", counter.load(Ordering::SeqCst));
}適切なツールの選び方
共有状態についての簡単な指針です。
- Channel:所有権を移動するパイプライン型。
- Arc<Mutex>:読み書きが混在する、可変の共有構造。
- Arc<RwLock>:読み取り中心の共有データ。
- Atomics:単一のプリミティブ型のカウンターやフラグ。
目的に合う最も単純なツールを優先し、メッセージパッシングでは問題をうまく表現できない場合にだけロックを使いましょう。
理解度チェック
共有状態の理解度を確認しましょう。
まとめ
スレッド間で状態を安全に共有する方法を学びました。
Arcはスレッドセーフな共有所有権を実現しますが、Rcは実現しません。Mutexは、ガードによる排他的な変更を提供し、自動的にロックを解除します。Arc<Mutex<T>>は、可変データを共有する標準的なパターンです。- ロックのスコープは短く保ち、デッドロックとpoisoningに注意します。
RwLockは読み取り中心のデータに、アトミック型は単純なカウンターに適しています。
よくある質問
「Arc/Mutexによる状態共有」レッスンは無料ですか?
はい。「Arc/Mutexによる状態共有」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Learn Rust Codingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Learn Rust Codingコースには全4レッスンが含まれています。
「Arc/Mutexによる状態共有」で何を学びますか?
安全な共有データ ブラウザで直接実行するハンズオンコードでLearn Rust Codingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Learn Rust Codingを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのLearn Rust Codingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「Arc/Mutexによる状態共有」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このLearn Rust Codingレッスンでコードを書いて実行できますか?
はい。すべてのLearn Rust Codingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- mpscチャネル
- Arc/Mutexによる状態共有
- スコープ付きスレッド
- Crossbeamチャネル