Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Concurrency Primitives

Rule

Choose by ownership shape: owned values first, channels for transfer, standard locks for short synchronous sections, and Tokio primitives for async coordination.

Why

The smallest primitive matching data ownership is easier to reason about and less likely to block the runtime accidentally.

Activation

Apply this page for channels, locks, atomics, worker pools, shared state, or runtime coordination.

Do

  • Prefer one clear owner for mutable state.
  • Use bounded channels when values or commands move to an owner.
  • Use Arc<T> for cross-thread or cross-task shared ownership.
  • Use standard Mutex or RwLock for short synchronous critical sections.
  • Use Tokio locks, semaphores, notifications, or channels when waiting is async.
  • Copy or clone needed data out before .await.
  • Start with Mutex; adopt RwLock only for meaningful read contention.
  • Use atomics only for simple flags, counters, or low-level coordination with obvious ordering.
  • Document ordering when code can hold multiple locks.
  • Follow blocking-work policy for pools and work isolation.

Avoid

  • Do not choose a Tokio mutex solely because the caller is async.
  • Do not hold a standard lock guard across .await.
  • Do not use Arc<Mutex<T>> to avoid selecting an owner.
  • Do not use channels for simple shared counters or snapshots.
  • Do not use unbounded channels without an explicit memory-growth argument.
  • Do not default to RwLock or OS threads.

Async Notes

Blocking-API lints are project guardrails, not a blanket ban on std::sync; see the new-project workflow.

Example

#![allow(unused)]
fn main() {
pub async fn handle_job(cache: &Mutex<Cache>, client: &Client, key: Key) -> Result<(), Error> {
    let path = {
        let mut cache = cache.lock().expect("cache mutex should not be poisoned");
        cache.entry(key).or_insert_with(default_path).clone()
    };

    let bytes = read_file(path).await?;
    client.upload(bytes).await
}
}

Exceptions

  • Use an async lock when waiting must be async or a guard intentionally spans .await.
  • Use an unbounded channel for a naturally bounded stream or documented best-effort telemetry.
  • Use an OS thread for affinity or long-lived blocking ownership, with a lint reason.