rust-concurrency について
Rust 開発者向けの並行処理スキルです。データ並列、スコープ付きスレッド、チャネル、共有状態から、負荷に合う方式を選ぶ方法と同期・安全性の注意点を説明します。
主な機能
- 01負荷に応じた並行モデルの選択
- 02Rayon によるデータ並列処理
- 03スコープ付きスレッドでスタックデータを借用
- 04チャネルによる所有権の受け渡し
- 05ロックとアトミック順序の指針
活用シーン
- 01独立した重い処理の並列化
- 02スレッド間でのスタックデータ共有
- 03共有ロックを使わないタスクの受け渡し
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill rust-concurrencyインストール後、Claude Code で/rust-concurrencyを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yaml
詳細
Rust の並行処理
ワークロードの形によってモデルが決まります。このスキルではスレッドに関する選択を扱い、タスク並行処理が始まるところで扱いを終えます。ランタイム、エグゼキューター、spawn_blocking、キャンセルについては /async-rust を参照してください。
ワークロードの形でモデルを選ぶ
- 多数の独立した項目に同じ操作を行う場合:データ並列処理、
rayon。 - 独立した作業単位の大半が待機状態にある場合:タスク並行処理。ここではなく
/async-rustを参照してください。 - 複数のスレッドが読み書きする状態がある場合:共有状態。デッドロックが起きる可能性があるのは3つのうちこれだけなので、最後に検討してください。
データ並列処理
par_iter() は、既存のイテレーターチェーンに対する一単語の変更です。まさにそのために、チェーンを書く価値があります。前提条件として、スケジューリングのコストに見合うだけの処理量が各項目に必要です。コストの低いクロージャーを100万個処理する par_iter は、多くの場合、かえって遅くなります。これはよくある期待外れの例です。
スコープ付きスレッド
std::thread::scope を使うと、スレッドからスタック上のデータを借用できます。スコープがスタックの巻き戻し前にスレッドの終了を保証するためです。これによって、通常は Arc を強制する 'static 境界が不要になります。
fn sum_halves(data: &[u64]) -> u64 {
let (left, right) = data.split_at(data.len() / 2);
std::thread::scope(|s| {
let a = s.spawn(|| left.iter().sum::<u64>());
let b = s.spawn(|| right.iter().sum::<u64>());
a.join().unwrap() + b.join().unwrap()
})
}
Arc やクローンを使う前に、スコープを使う方法を検討してください。
引き渡しにチャネルを使う
チャネルではメッセージとともに所有権が移動するため、保持するロックも、poison 状態になるものもありません。送信側がドロップされると、それがシャットダウンのシグナルになります。recv は切断を報告します。チャネルの有界・無界の違いはバックプレッシャーに関わります。無界チャネルは、スループットの問題をひそかにメモリの問題へと変えてしまいます。
共有状態
デフォルトは Mutex です。RwLock が有効なのは実際に読み取りが多い場合に限られ、書き込み側が飢餓状態になる可能性もあります。保持中にパニックが発生した Mutex は Err を返します。エラーを伝播させるか、理由を明記したうえで into_inner() を使ってください。反射的に unwrap を使ってはいけません。ガードを保持する範囲は可能な限り短くし、再度ロックする可能性のあるコードの呼び出しをまたいで保持してはいけません。
アトミック操作とメモリーオーダリング
原則は次のとおりです。誰とも同期しないカウンターには Relaxed、一方のスレッドが公開し、もう一方が観測する対になった引き渡しには Acquire/Release、確信が持てずコストも許容できる場合には SeqCst を使います。最も間違えやすいのは、次の対です。
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
fn main() {
let value = Arc::new(AtomicU64::new(0));
let ready = Arc::new(AtomicBool::new(false));
let (v, r) = (Arc::clone(&value), Arc::clone(&ready));
let publisher = std::thread::spawn(move || {
v.store(42, Ordering::Relaxed);
r.store(true, Ordering::Release);
});
while !ready.load(Ordering::Acquire) {
std::thread::yield_now();
}
assert_eq!(value.load(Ordering::Relaxed), 42);
publisher.join().unwrap();
}
Release ストアは、ready が切り替わる前に value への書き込みが順序付けられるようにし、Acquire ロードはそれと対になります。順序付けの根拠を2文で書けないなら、そのコードでは Mutex を使うべきです。
手動の Send/Sync は unsafe に関する主張です
unsafe impl Send for T は、コンパイラーが証明できなかったことを断言するものです。// SAFETY: コメントには実際の根拠を記載しなければなりません。健全性のレビューについては /unsafe-rust を参照してください。このスキルでは、その主張が記述されていることだけを求めます。
static mut ではなく thread_local!
現在のエディションでは static mut は実質的に使えず、共有することが健全だったこともありません。スレッドごとの状態には thread_local! を使います。状態を渡すほうが、グローバルに隠すより優れています。グローバルのライフタイムは、誰も明示していないライフタイムです。
対象外の事項
タスク並行処理、エグゼキューター、spawn_blocking、キャンセルについては /async-rust を参照してください。そもそも状態を共有すべきかどうかについては /ownership-not-clone を参照してください。unsafe impl の健全性については /unsafe-rust を参照してください。並列版のスループットについては /rust-performance を参照してください。
検証
cargo test # including the ignored-by-default stress tests, if the repo has any
cargo clippy --all-targets # at the level the repo configures
cargo miri test # where atomics or unsafe Send/Sync are involved
Clippy は、対象リポジトリで設定されている lint レベルで実行します。-D warnings を使うのは、リポジトリで lint がまったく設定されていない場合に限ります。ロックフリー構造と複雑な順序付けの根拠には loom を使います。スケジューラーが問題のある順序を選ぶことを期待するのではなく、すべてのインターリーブを網羅的に探索します。独自の cfg のもとで実行されるため、モデルは小さくする必要があります。