| name | rust-async |
| description | | Use when this capability is needed. |
Async Rust Programming
Comprehensive guide to async programming with Tokio, futures, and concurrent patterns.
Quick Navigation
- references/tokio.md - Tokio runtime deep dive
- references/patterns.md - Async patterns and anti-patterns
- references/channels.md - Channel types and message passing
Core Concepts
Async/Await Basics
async fn fetch_data(url: &str) -> Result<String, Error> {
let response = reqwest::get(url).await?;
let body = response.text().await?;
Ok(body)
}
let future = fetch_data("https://api.example.com");
let result = future.await;
The Tokio Runtime
#[tokio::main]
async fn main() {
println!("Hello, async world!");
}
fn main() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
println!("Hello, async world!");
});
}
#[tokio::main]
async fn main() { ... }
#[tokio::main(flavor = "current_thread")]
async fn main() { ... }
#[tokio::main(worker_threads = 4)]
async fn main() { ... }
Spawning Tasks
tokio::spawn
Run tasks concurrently:
use tokio::task;
#[tokio::main]
async fn main() {
let handle1 = task::spawn(async {
"result1"
});
let handle2 = task::spawn(async {
"result2"
});
let (result1, result2) = tokio::join!(handle1, handle2);
println!("{:?}, {:?}", result1, result2);
}
spawn_blocking
For CPU-bound work:
let result = tokio::task::spawn_blocking(|| {
expensive_hash_computation()
}).await?;
let result = tokio::task::spawn_blocking(move || {
std::fs::read_to_string("large_file.txt")
}).await??;
Task Types
| Function | Use Case | Thread Pool |
|---|
spawn | Async work | Async workers |
spawn_blocking | CPU/blocking work | Blocking pool |
spawn_local | Non-Send futures | Current thread |
block_in_place | Blocking in async context | Current thread |
Concurrency Primitives
tokio::join! - Run Concurrently, Wait for All
use tokio::join;
async fn fetch_all() -> Result<(User, Posts, Comments), Error> {
let (user, posts, comments) = join!(
fetch_user(),
fetch_posts(),
fetch_comments()
);
Ok((user?, posts?, comments?))
}
tokio::select! - First to Complete Wins
use tokio::{select, time::{sleep, Duration}};
async fn with_timeout() -> Result<Data, Error> {
select! {
result = fetch_data() => result,
_ = sleep(Duration::from_secs(5)) => {
Err(Error::Timeout)
}
}
}
select! {
msg = rx.recv() => handle_message(msg),
_ = shutdown_signal() => break,
_ = interval.tick() => do_periodic_work(),
}
FuturesUnordered - Dynamic Task Set
use futures::stream::{FuturesUnordered, StreamExt};
async fn process_urls(urls: Vec<String>) -> Vec<Result<String, Error>> {
let mut futures = FuturesUnordered::new();
for url in urls {
futures.push(fetch(url));
}
let mut results = Vec::new();
while let Some(result) = futures.next().await {
results.push(result);
}
results
}
Channels
MPSC (Multi-Producer, Single-Consumer)
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<String>(100);
let tx2 = tx.clone();
tokio::spawn(async move {
tx.send("Hello".to_string()).await.unwrap();
});
tokio::spawn(async move {
tx2.send("World".to_string()).await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("Got: {}", msg);
}
}
Oneshot (Single Value)
use tokio::sync::oneshot;
async fn request_response() {
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
let result = compute().await;
tx.send(result).unwrap();
});
let result = rx.await.unwrap();
}
Broadcast (Multi-Consumer)
use tokio::sync::broadcast;
let (tx, _rx) = broadcast::channel::<Event>(100);
let mut rx1 = tx.subscribe();
let mut rx2 = tx.subscribe();
tx.send(Event::Shutdown)?;
Watch (Single Value, Multiple Observers)
use tokio::sync::watch;
let (tx, rx) = watch::channel(Config::default());
tx.send(new_config)?;
let current = rx.borrow().clone();
rx.changed().await?;
Channel Comparison
| Type | Producers | Consumers | Buffering |
|---|
mpsc | Many | One | Bounded/Unbounded |
oneshot | One | One | Single value |
broadcast | One | Many | Bounded |
watch | One | Many | Latest only |
Synchronization
Mutex
use tokio::sync::Mutex;
use std::sync::Arc;
let data = Arc::new(Mutex::new(vec![]));
let data2 = data.clone();
tokio::spawn(async move {
let mut guard = data2.lock().await;
guard.push(1);
});
let item = {
let guard = data.lock().await;
guard.first().cloned()
};
process(item).await;
RwLock
use tokio::sync::RwLock;
let data = RwLock::new(HashMap::new());
let reader1 = data.read().await;
let reader2 = data.read().await;
let mut writer = data.write().await;
writer.insert("key", "value");
Semaphore
use tokio::sync::Semaphore;
use std::sync::Arc;
let semaphore = Arc::new(Semaphore::new(10));
async fn limited_task(sem: Arc<Semaphore>) {
let permit = sem.acquire().await.unwrap();
do_work().await;
}
Async I/O
File Operations
use tokio::fs;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader, BufWriter};
let content = fs::read_to_string("file.txt").await?;
fs::write("output.txt", content).await?;
let file = fs::File::open("large.txt").await?;
let mut reader = BufReader::new(file);
let mut line = String::new();
reader.read_line(&mut line).await?;
TCP
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (socket, addr) = listener.accept().await?;
tokio::spawn(async move {
handle_connection(socket).await;
});
}
let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
stream.write_all(b"Hello").await?;
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).await?;
Common Patterns
Graceful Shutdown
use tokio::signal;
use tokio::sync::watch;
#[tokio::main]
async fn main() {
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let server = tokio::spawn(run_server(shutdown_rx.clone()));
signal::ctrl_c().await.unwrap();
shutdown_tx.send(true).unwrap();
server.await.unwrap();
}
async fn run_server(mut shutdown: watch::Receiver<bool>) {
loop {
select! {
conn = accept_connection() => handle(conn).await,
_ = shutdown.changed() => {
if *shutdown.borrow() {
break;
}
}
}
}
}
Rate Limiting
use tokio::time::{interval, Duration};
async fn rate_limited_work() {
let mut interval = interval(Duration::from_millis(100));
for item in items {
interval.tick().await;
process(item).await;
}
}
Timeout
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(5),
fetch_data()
).await;
match result {
Ok(data) => println!("Got data: {:?}", data),
Err(_) => println!("Timed out"),
}
Retry with Backoff
use tokio::time::{sleep, Duration};
async fn retry_with_backoff<F, Fut, T, E>(
mut f: F,
max_retries: u32,
) -> Result<T, E>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, E>>,
{
let mut delay = Duration::from_millis(100);
for attempt in 0..max_retries {
match f().await {
Ok(result) => return Ok(result),
Err(e) if attempt + 1 == max_retries => return Err(e),
Err(_) => {
sleep(delay).await;
delay *= 2;
}
}
}
unreachable!()
}
Structured Concurrency: JoinSet
Manage a dynamic set of tasks with automatic cleanup:
use tokio::task::JoinSet;
async fn process_all(items: Vec<Item>) -> Vec<Result<Output, Error>> {
let mut set = JoinSet::new();
for item in items {
set.spawn(async move { process(item).await });
}
let mut results = Vec::new();
while let Some(result) = set.join_next().await {
results.push(result.unwrap());
}
results
}
async fn limited_parallel(items: Vec<Item>, limit: usize) {
let mut set = JoinSet::new();
for item in items {
if set.len() >= limit {
set.().;
}
set.( { (item). });
}
set.()..() {}
}
Cancellation
CancellationToken (tokio-util)
use tokio_util::sync::CancellationToken;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let token = CancellationToken::new();
let worker_token = token.clone();
let worker = tokio::spawn(async move {
loop {
tokio::select! {
_ = worker_token.cancelled() => {
println!("Worker shutting down cleanly");
break;
}
_ = do_work() => {}
}
}
});
sleep(Duration::from_secs(2)).await;
token.cancel();
worker.await.unwrap();
}
let handle = tokio::spawn(async { loop { work().await; } });
drop(handle);
token.();
Timeout as Cancellation
use tokio::time::{timeout, Duration};
match timeout(Duration::from_secs(5), long_running_task()).await {
Ok(result) => println!("Completed: {:?}", result),
Err(_elapsed) => println!("Timed out — task was cancelled"),
}
Anti-Patterns
Holding Locks Across Await
let guard = mutex.lock().await;
some_async_operation().await;
drop(guard);
let data = {
let guard = mutex.lock().await;
guard.clone()
};
some_async_operation_with(data).await;
Blocking in Async Context
async fn bad() {
std::thread::sleep(Duration::from_secs(1));
std::fs::read_to_string("file.txt");
}
async fn good() {
tokio::time::sleep(Duration::from_secs(1)).await;
tokio::fs::read_to_string("file.txt").await;
}
let result = tokio::task::spawn_blocking(|| {
blocking_library_call()
}).await?;
Creating Runtime in Async Context
async fn bad() {
tokio::runtime::Runtime::new().unwrap()
.block_on(async { ... });
}
async fn good() {
some_future().await;
}
Best Practices from the Field
1. Always Prefer Bounded Channels
Avoid unbounded channels (like tokio::sync::mpsc::unbounded_channel) in production pipelines. Without backpressure, slow consumers will cause memory leaks. Always specify a bound (capacity):
let (tx, mut rx) = tokio::sync::mpsc::channel(100);
2. Use JoinSet for Dynamic Task Groups
Instead of collecting handles and iterating or using unstable futures::future::join_all, use tokio::task::JoinSet to manage lifetimes of dynamically spawned concurrent workers.
use tokio::task::JoinSet;
let mut set = JoinSet::new();
for i in 0..10 {
set.spawn(async move { i * 2 });
}
while let Some(res) = set.join_next().await {
println!("Task finished: {:?}", res?);
}
Debugging Tips
- Use
#[tokio::test] for async tests
- Enable tracing with
tracing crate
- Use
tokio-console for runtime inspection (cargo install tokio-console)
- Check for dropped futures (no
.await)
- Look for blocking calls in async code
CancellationToken over Arc<AtomicBool> — atomic flags don't wake sleeping tasks
Production Checklist
- Bound channels and queues to preserve backpressure.
- Use
JoinSet or structured task ownership for dynamic task groups.
- Add cancellation paths for long-running tasks.
- Avoid blocking calls on runtime worker threads; use async APIs or
spawn_blocking.
- Instrument tasks with
tracing spans and inspect with tokio-console when needed.
- Test shutdown and cancellation, not only happy-path completion.
References
Source: adxptived/Rust-Skills — distributed by TomeVault.