agentleFS
dawidpereiradawidpereira/rust-skills/skills/rust-async/SKILL.md

rust-async

Skill0 starsChanged 8 months agoMITView on GitHub

Async Rust and concurrency with Tokio. Use when writing async code, choosing channel types (mpsc/broadcast/watch/oneshot), dealing with Send/Sync bounds, spawn_blocking, JoinSet, CancellationToken, or fixing issues with locks held across .await points. Also use for tokio::select!, graceful shutdown, and structured concurrency patterns.

4 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 4 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never hold a std::sync::Mutex guard across an .await:

    line 80
  2. Distinctive:

    Prefer restructuring to avoid holding locks across await entirely.

    line 94
  3. Distinctive:

    Don't await them sequentially — that's 5x slower.

    line 104

“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Async & Concurrency

Core Question

Is this I/O-bound (async) or CPU-bound (threads/spawn_blocking)?

Async is for waiting on external things (network, disk, timers). CPU-heavy work blocks the runtime — move it to spawn_blocking or a dedicated thread pool.


Error → Design Question

SymptomDon't Just SayAsk Instead
Future is not Send"Add Send bound"Why does data cross a thread boundary? Can you restructure?
Deadlock with Mutex"Use tokio::sync::Mutex"Should you hold a lock across await at all?
Task hangs forever"Add timeout"Is there a cancellation path?
Channel fills up"Make it unbounded"What's the backpressure strategy?

Quick Decisions

SituationReach ForWhy
Run independent futures concurrentlytokio::join!Runs all, returns all results
Run fallible futures, fail fasttokio::try_join!Returns first error, drops rest
Race futures, handle first completiontokio::select!Cancel losers automatically
Dynamic number of spawned tasksJoinSetAdd/remove tasks, collect results
CPU-intensive work in async contextspawn_blockingMoves to blocking thread pool
File I/O in async codetokio::fsNon-blocking file operations
Graceful shutdownCancellationTokenHierarchical cancellation
One producer, one consumer, one messageoneshotRequest-response pattern
Work queue (multiple producers)mpsc (bounded)Backpressure built in
All subscribers get all messagesbroadcastPub/sub pattern
Share latest value, skip intermediatewatchConfig updates, state sharing
Shared read-only data across tasksArc<T>Clone Arc, not the data
Shared mutable state across tasksArc<Mutex<T>>Or Arc<RwLock<T>> if reads dominate
Stream of async valuestokio_stream + StreamExtAsync equivalent of Iterator
Paginated API consumptionstream::unfoldLazy, backpressure-aware page fetching
CPU-bound data parallelismrayon::par_iterAutomatic work-stealing across cores
CPU-bound work in async contextspawn_blocking + rayonKeep the async runtime unblocked
Scoped parallel work (no Arc)std::thread::scopeBorrows stack data safely across threads
Atomic flag or counterAtomicBool / AtomicUsizeLock-free, single-word synchronization
async fn in trait definitionnative async fn in traitsNo #[async_trait] needed since Rust 1.75

Channel Selection

ChannelPatternCapacityReceivers
oneshotRequest → Response1 message1
mpscWork queueBounded (set capacity)1
broadcastPub/sub (all get all)Bounded (ring buffer)N (all messages)
watchLatest value1 (latest only)N (skip to newest)

Always use bounded channels unless you have a specific reason not to. Unbounded channels grow without limit when producer outpaces consumer.

Buffer sizing: start with num_producers * 2 or expected burst size. Monitor with capacity() and len().


The Lock-Across-Await Problem

Never hold a std::sync::Mutex guard across an .await:

// Bad: guard held across await — can deadlock
let mut guard = data.lock().unwrap();
*guard = fetch().await;

// Good: extract, await, then lock again
let current = data.lock().unwrap().clone();
let new_data = process(current).await;
*data.lock().unwrap() = new_data;

tokio::sync::Mutex is await-safe but has higher overhead. Prefer restructuring to avoid holding locks across await entirely.


Usage Scenarios

Scenario 1: "I need to make 5 HTTP requests and combine the results" → Use tokio::try_join! for a fixed number, or JoinSet for a dynamic number. Don't await them sequentially — that's 5x slower.

Scenario 2: "My async task needs to do JSON parsing on large payloads" → JSON parsing is CPU-bound. Use spawn_blocking(move || serde_json::from_str(&data)) to avoid blocking the runtime.

Scenario 3: "I need to shut down gracefully when Ctrl+C is pressed" → Create a CancellationToken, pass child tokens to tasks, and use tokio::select! to race work against token.cancelled(). On signal, cancel the root token.


Reference Files

FileRead When
references/tokio-patterns.mdRuntime setup, spawn_blocking, join/select patterns, JoinSet, cancellation
references/channels.mdChoosing and using mpsc/broadcast/watch/oneshot, backpressure, message patterns
references/safety.mdLock safety across await, Send/Sync issues, clone-before-await patterns
references/streams.mdStream trait, StreamExt, Pin, async fn in traits, paginated/WebSocket patterns
references/threads-and-parallelism.mdstd::thread, rayon, crossbeam, atomics, async-vs-threads decision

Cross-References

WhenCheck
Smart pointers for shared state (Arc, Mutex)rust-ownership → Quick Decisions
Error handling in async (try_join, ?)rust-errors → Quick Decisions
Async trait design and Send boundsrust-types → Quick Decisions
Tokio runtime profile settingsrust-perf → Quick Decisions
Tracing spans in async code, .instrument()rust-tracing → Quick Decisions
Rayon, parallel iteratorsrust-perf → Quick Decisions

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.