項目17:状態共有並列実行には気を付けよう
並列処理と Rust
状態を共有する 並列処理 は以下の 3 つの問題を引き起こす 競合状態: 実行のタイミング次第で不整合な状態になる radish-miyazaki.icon が追加
これらの問題の多くは 非決定的 に発生し、負荷がかかった時に起こりやすいため、デバッグ が困難 まとめ
状態共有並列に起因する問題を避けるには、単に状態共有並列を避けることである
具体的には、
Do not communicate by sharing memory; instead, share memory by communicating.
(メモリを共有して通信してはいけない; 通信してメモリを共有しよう。)
Go ではこの目的に適した チャネル を言語自体に組み込んでいる channel 関数を((Sender, Receiver) ペアを返す)を用いると特定の型の値をスレッド間で受け渡すことができる もし状態共有並列が避けられない場合は、デッドロック を避けるためにいかに気をつけよう 相互に整合する必要があるデータ構造は 1 つの mutex で管理しよう ロックのスコープは小さく自明にしよう
可能ならば、ロックの取得・解放を 1 箇所にまとめたヘルパメソッドも用意しよう
ロックを保持したままクロージャを呼び出すのはやめよう ?? ∵ クロージャ内でロックを保持すると、後から追加されるコード次第でデッドロックが発生するリスクがあるため
e.g.
code:rs
struct SharedState {
data1: Mutex<i32>,
data2: Mutex<i32>,
}
impl SharedState {
fn with_lock<F>(&self, f: F)
where
F: FnOnce(&mut i32),
{
let mut data = self.data1.lock().unwrap();
f(&self.data2); // ロックを保持したままクロージャを実行
}
}
with_lock のクロージャ内で他のロックを取るコードが追加されると、デッドロックが発生しうる
code:rs
let cloned_state = Arc::clone(&state);
let handle = thread::spawn(move || {
state.with_lock(|val| {
*val += 1;
let _lock2 = mutex.lock().unwrap();
});
});
let cloned_state2 = Arc::clone(&state);
let handle2 = thread::spawn(move || {
let _lock2 = cloned_state2.data2.lock().unwrap();
let _lock1 = cloned_state2.data1.lock().unwrap();
});
MutexGuard を呼び出し元に返さないようにしよう
∵ MutexGuard はドロップされた時点でロックを解除するが、呼び出し元に戻すことでロックのスコープが不明確になり、デッドロックや長時間のロック保持のリスクが増すため
ロックの取得順番を定めた「ロック階層」を設計して、ドキュメント化し、テストし、厳格に運用しよう
ただし、「エンジニアがミスをしないことを前提とする戦略」は長期的に失敗する ため、最後の手段とすべき
マルチスレッドコードでは、「複雑すぎて間違っていることが明らかでないコードではなく、簡単すぎて明らかに間違っていないことが分かるコードを書こう」