rust-tokio
Skill0 starsChanged 35 days agoMITView on GitHub
Use when working with async Rust and tokio.
In this repository15 other files it gives its agents.
- AGENTS.mdAGENTS.md
- rustoriaSkill
- rust-debuggingSkill
- rust-dependenciesSkill
- rust-fundamentalsSkill
- rust-axumSkill
- rust-clapSkill
- rust-dieselSkill
- rust-ratatuiSkill
- rust-rayonSkill
- rust-serdeSkill
- rust-productionSkill
- rust-code-auditSkill
- rust-stdSkill
- rust-unsafe-checkerSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 65
No blocking — other tasks can run while waiting
- Very specific: line 172
Short critical sections never held across
.await: use std::sync::Mutex. It's cheaper, needs no .await to lock, and is the recommended … - Very specific: line 175
Guard must be held across
.await: use tokio::sync::Mutex (its guard is Send). - Very specific: line 204
If the critical section must span an .await, swap in tokio::sync::Mutex and use counter.lock().await.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Tokio — Async Runtime for Rust
Overview
Tokio is an event-driven, non-blocking I/O platform for writing asynchronous applications. It provides:
- Multithreaded, work-stealing task scheduler
- Reactor backed by OS event queue (epoll, kqueue, IOCP)
- Async TCP/UDP sockets
- Timers, channels, and synchronization primitives
Installation
[dependencies]
tokio = { version = "1", features = ["full"] } # 1.x series (1.53 as of late 2026)Core Concepts
1. The Runtime
#[tokio::main]
async fn main() {
// This runs on the tokio runtime
println!("Hello from tokio!");
}The #[tokio::main] macro sets up a multi-threaded runtime. For custom configuration:
// Multi-threaded runtime with 4 worker threads
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
println!("running on 4 worker threads");
}
// Current-thread runtime (single-threaded execution; tokio::spawn still requires Send -
// use LocalSet + spawn_local for !Send tasks)
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("running on one thread");
}2. Async Functions
async fn fetch_data() -> String {
// .await suspends until the timer fires; other tasks run meanwhile
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
"data".to_string()
}async fnreturns aFuture.awaitsuspends execution until the future completes- No blocking — other tasks can run while waiting
3. Spawning Tasks
Futures passed to tokio::spawn must be Send + 'static:
Send— the work-stealing scheduler can move a task to another worker thread between.awaitpoints, so the future and everything it holds must be safe to transfer across threads. (Sync— safe to share&Tacross threads — matters when tasks share references.) Most types are both; notable non-Sendtypes:Rc,RefCell,std::sync::MutexGuard.'static— the task may outlive the function that spawned it, so it cannot borrow stack data. Every value alive across an.awaitbecomes part of the future's state; move in owned values orArc.
#[tokio::main]
async fn main() {
// Spawn a new task onto the runtime
let handle = tokio::spawn(async {
println!("Running in a spawned task");
42
});
// Await the task's result (JoinError if the task panicked or was aborted)
let result = handle.await.unwrap();
println!("Result: {}", result);
}4. Join Multiple Tasks
use std::time::Duration;
use tokio::time::sleep;
async fn fetch_data() -> i32 {
sleep(Duration::from_millis(10)).await;
1
}
async fn fetch_other() -> i32 {
sleep(Duration::from_millis(20)).await;
2
}
async fn fallible1() -> Result<i32, &'static str> { Ok(1) }
async fn fallible2() -> Result<i32, &'static str> { Ok(2) }
#[tokio::main]
async fn main() {
// Both run concurrently; waits for both
let (result1, result2) = tokio::join!(fetch_data(), fetch_other());
println!("{result1} {result2}");
// try_join! returns early on the first error
let (r1, r2) = tokio::try_join!(fallible1(), fallible2()).unwrap();
println!("{r1} {r2}");
}5. Select — Race Multiple Futures
use std::time::Duration;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
let slow_future = sleep(Duration::from_secs(5));
tokio::select! {
_ = sleep(Duration::from_secs(1)) => {
println!("Timeout!");
}
_ = slow_future => {
println!("Completed first");
}
}
}The losing branch's future is dropped mid-execution — see §11 for cancellation-safety.
6. Channels
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32); // bounded: buffer size 32
tokio::spawn(async move {
for i in 0..10 {
tx.send(i).await.unwrap(); // demo-grade unwrap: fails only if rx dropped
}
});
while let Some(value) = rx.recv().await {
println!("Received: {}", value);
}
}7. Shared State
Choosing the mutex (this matches Tokio's own docs):
- Short critical sections never held across
.await: usestd::sync::Mutex. It's cheaper, needs no.awaitto lock, and is the recommended default. Just ensure the guard drops before any.await— astd::sync::MutexGuardheld across.awaitmakes the future!Send. - Guard must be held across
.await: usetokio::sync::Mutex(its guard isSend). Beware: it serializes tasks, so a contended async mutex becomes a bottleneck.
use std::sync::{Arc, Mutex}; // std Mutex: short critical sections only
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = tokio::spawn(async move {
// Guard is created, used, and dropped with no .await in between
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
println!("Counter: {}", *counter.lock().unwrap());
}If the critical section must span an .await, swap in tokio::sync::Mutex and use counter.lock().await.
8. Timers
use std::time::Duration;
use tokio::time::{interval, sleep};
async fn some_slow_operation() -> i32 {
sleep(Duration::from_secs(10)).await;
42
}
#[tokio::main]
async fn main() {
// Sleep
sleep(Duration::from_secs(1)).await;
// Timeout
let result = tokio::time::timeout(Duration::from_secs(5), some_slow_operation()).await;
assert!(result.is_err()); // Elapsed
// Interval (first tick completes immediately)
let mut interval = interval(Duration::from_secs(1));
for _ in 0..3 {
interval.tick().await;
println!("Tick");
}
}9. Async I/O
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
async fn echo(socket: &mut TcpStream) -> std::io::Result<()> {
let mut buf = [0; 1024];
loop {
let n = socket.read(&mut buf).await?;
if n == 0 {
return Ok(()); // peer closed the connection
}
socket.write_all(&buf[..n]).await?;
}
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, _) = listener.accept().await?;
tokio::spawn(async move {
if let Err(e) = echo(&mut socket).await {
eprintln!("connection error: {e}");
}
});
}
}10. Async Traits
async fn in traits is stable since Rust 1.75 — this is the default choice, no macro needed:
trait Database {
async fn fetch_user(&self, id: u64) -> Option<String>;
}
struct Pg;
impl Database for Pg {
async fn fetch_user(&self, id: u64) -> Option<String> {
Some(format!("user-{id}"))
}
}Limitation: a trait with native async fn is not dyn-compatible (Box<dyn Database> fails to compile). When you
need trait objects, use the async-trait crate, which boxes the returned
futures to restore object safety:
[dependencies]
async-trait = "0.1"use async_trait::async_trait;
#[async_trait]
trait DynDatabase {
async fn fetch_user(&self, id: u64) -> Option<String>;
}
// Now `Arc<dyn DynDatabase>` / `Box<dyn DynDatabase>` work.11. Cancellation
Three mechanisms to know:
- Drop-based cancellation: dropping a future cancels it. Cancellation takes effect at the next
.awaityield point — CPU-bound code that never.awaits cannot be cancelled until it yields. Note: dropping aJoinHandleonly detaches the task (it keeps running); useJoinHandle::abort()to cancel a spawned task. select!races: the losing branch's future is dropped wherever it was suspended. That is only safe if dropping it loses no state — "cancel safety".tokio::sync::mpsc::Receiver::recvandAsyncReadExt::readare cancel-safe; a hand-rolled future that has partially consumed input may drop that input. Check the "Cancel safety" section of each tokio API's docs before using it inselect!.CancellationToken(fromtokio-util): explicit, cooperative shutdown for many tasks at once.
[dependencies]
tokio-util = "0.7"use std::time::Duration;
use tokio_util::sync::CancellationToken;
async fn worker(token: CancellationToken) {
loop {
tokio::select! {
_ = token.cancelled() => {
println!("Shutting down");
return;
}
_ = tokio::time::sleep(Duration::from_secs(1)) => {
println!("Working...");
}
}
}
}12. Best Practices
- Don't block in async code —
std::thread::sleep, sync file I/O, and blocking DB drivers stall the executor. Wrap blocking calls intokio::task::spawn_blocking. For CPU-bound parallelism use rayon instead (see the rust-rayon skill). - Use
Arcfor shared state across tasks. - Pick the right mutex —
std::sync::Mutexfor short sections never held across.await;tokio::sync::Mutexonly when the guard must live across.await(see §7). - Use
?for error propagation in async functions. - Task granularity is a trade-off — every task pays scheduling and memory overhead, so don't spawn one task per tiny item; but too few coarse tasks waste parallelism. Batch small units; split when units are large or must make progress independently.
- Use
JoinSetfor managing many tasks. - Backpressure — use bounded channels (
mpsc::channel(n)) so fast producers can't exhaust memory. - Graceful shutdown — use
CancellationTokenortokio::signal.
13. Common Patterns
use std::sync::Arc;
use std::time::Duration;
async fn do_work(i: i32) -> i32 {
tokio::time::sleep(Duration::from_millis(10)).await;
i * 2
}
fn blocking_call() -> i32 {
// e.g. a synchronous database driver or C FFI call
42
}
#[tokio::main]
async fn main() {
// spawn_blocking: run BLOCKING (synchronous) code on a dedicated thread pool.
// For CPU-bound parallelism, use rayon instead.
let result = tokio::task::spawn_blocking(blocking_call).await.unwrap();
println!("blocking result: {result}");
// JoinSet: manage a dynamic set of tasks
let mut set = tokio::task::JoinSet::new();
for i in 0..10 {
set.spawn(do_work(i));
}
while let Some(res) = set.join_next().await {
println!("completed: {:?}", res);
}
// Semaphore: limit concurrency
let sem = Arc::new(tokio::sync::Semaphore::new(3));
let permit = sem.clone().acquire_owned().await.unwrap();
drop(permit); // released back to the semaphore
}14. Feature Flags
# Minimal
tokio = { version = "1", features = ["rt"] }
# Common
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "io-util"] }
# Everything
tokio = { version = "1", features = ["full"] }When to Use Tokio
- Web servers (with axum, actix, etc.)
- Database connection pooling
- File I/O
- Network protocols
- Background task processing
- Any I/O-bound concurrent workload
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
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.

