mcpField
R
rust-concurrency

rust-concurrency

github.com/rewrite-rsOther1039 installsUpdated 10w ago
Repository

About rust-concurrency

A Rust concurrency skill for developers choosing among data parallelism, scoped threads, channels, and shared state. It explains when each model fits and highlights synchronization and safety concerns.

Key features

  1. 01Choose a concurrency model by workload
  2. 02Use Rayon for data parallelism
  3. 03Borrow stack data with scoped threads
  4. 04Transfer ownership through channels
  5. 05Guidance on locks and atomic orderings

Use cases

  1. 01Parallelizing substantial per-item work
  2. 02Sharing stack data between threads
  3. 03Handing work off without shared locks

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/rewrite-rs/skills --skill rust-concurrency

After installing, type/rust-concurrencyin Claude Code to confirm it loaded.

Bundled filesagents/openai.yaml

Details

From the project README

Rust Concurrency

The shape of the workload picks the model. This skill owns that pick for threads, and it stops where task concurrency begins — runtime, executors, spawn_blocking, and cancellation live in /async-rust.

The shape picks the model

  • The same operation over many independent items: data parallelism, rayon.
  • Independent units of work that are mostly waiting: task concurrency, /async-rust, not here.
  • State several threads read and write: shared state — the one to reach for last, because it is the only one of the three that can deadlock.

Data parallelism

par_iter() is a one-word change to an iterator chain that already exists — exactly why the chain was worth writing. The precondition: the per-item work has to be large enough to pay for the scheduling. A par_iter over a million cheap closures is often slower, and is the standard disappointment.

Scoped threads

std::thread::scope lets a thread borrow stack data, because the scope guarantees the join before the stack unwinds — which is what removes the 'static bound that otherwise forces an Arc:

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()
    })
}

Reach for a scope before an Arc or a clone buys the second thread.

Channels for handoff

A channel moves ownership with the message — no lock to hold, nothing to poison. Sender-drop is the shutdown signal: recv reports disconnection. Bounded versus unbounded is backpressure: an unbounded channel silently converts a throughput problem into a memory problem.

Shared state

Mutex is the default; RwLock pays off only with genuinely read-heavy access, and can starve writers. A Mutex whose holder panicked hands back an Err — propagate it, or into_inner() with a written reason, never a reflexive unwrap. Hold the guard for the shortest possible scope, and never across a call into code that might lock again.

Atomics and orderings

The rule: Relaxed for a counter nobody synchronises on, Acquire/Release for a paired handoff where one thread publishes and another observes, SeqCst when unsure and the cost is acceptable. The pair is the one that is wrong most often:

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();
}

The Release store orders the write to value before ready flips, and the Acquire load pairs with it. If the ordering argument cannot be written down in two sentences, the code wants a Mutex.

Manual Send/Sync is an unsafe claim

unsafe impl Send for T asserts what the compiler could not prove; the // SAFETY: comment must hold an actual argument. The soundness review goes to /unsafe-rust — this skill only insists the claim is written.

thread_local! over static mut

static mut is effectively unusable in current editions and was never sound to share; thread_local! is the per-thread answer. Passing state beats hiding it in a global — a global is a lifetime nobody wrote down.

Deferrals

Task concurrency, executors, spawn_blocking, and cancellation are /async-rust. Whether the state should be shared at all is /ownership-not-clone. The soundness of an unsafe impl is /unsafe-rust. Throughput of the parallel version is /rust-performance.

Verification

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 runs at the lint level the target repo configures; -D warnings is the fallback only when the repo configures no lints at all. Lock-free structures and non-trivial ordering arguments want loom — it explores interleavings exhaustively rather than hoping the scheduler picks the bad one; it runs under its own cfg, and the model has to be small.

Comments

Markdown supported