mcpField
R
rust-concurrency

rust-concurrency

github.com/rewrite-rsその他1039 回インストール10 週間前に更新
リポジトリ

rust-concurrency について

Rust 開発者向けの並行処理スキルです。データ並列、スコープ付きスレッド、チャネル、共有状態から、負荷に合う方式を選ぶ方法と同期・安全性の注意点を説明します。

主な機能

  1. 01負荷に応じた並行モデルの選択
  2. 02Rayon によるデータ並列処理
  3. 03スコープ付きスレッドでスタックデータを借用
  4. 04チャネルによる所有権の受け渡し
  5. 05ロックとアトミック順序の指針

活用シーン

  1. 01独立した重い処理の並列化
  2. 02スレッド間でのスタックデータ共有
  3. 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 のもとで実行されるため、モデルは小さくする必要があります。

コメント