Loading pageā¦
Rust walkthroughs
Loading pageā¦
tokio::spawn and tokio::task::spawn_blocking for concurrent task execution?tokio::spawn schedules asynchronous tasks on Tokio's work-stealing runtime, while tokio::task::spawn_blocking offloads blocking operations to a dedicated thread pool. The critical distinction is that tokio::spawn runs tasks that yield control back to the runtime (async code that uses .await), whereas spawn_blocking is for code that would block the runtime threadāCPU-intensive work, synchronous I/O, or FFI calls. Blocking an async runtime thread prevents other tasks from running on that thread, reducing overall throughput. spawn_blocking isolates blocking work from the async runtime, preserving responsiveness.
#[tokio::main]
async fn main() {
// tokio::spawn schedules an async task on the runtime
let handle = tokio::spawn(async {
println!("Running on the async runtime");
42
});
// The task runs concurrently
let result = handle.await.expect("Task panicked");
println!("Result: {}", result);
}tokio::spawn creates an async task that runs on Tokio's runtime.
#[tokio::main]
async fn main() {
// spawn_blocking runs synchronous code on a blocking thread pool
let handle = tokio::task::spawn_blocking(|| {
println!("Running on blocking thread pool");
std::thread::sleep(std::time::Duration::from_millis(100));
42
});
let result = handle.await.expect("Task panicked");
println!("Result: {}", result);
}spawn_blocking runs blocking code on a separate thread pool.
#[tokio::main]
async fn main() {
// WRONG: Blocking the async runtime
tokio::spawn(async {
// This blocks the runtime thread!
std::thread::sleep(std::time::Duration::from_secs(5));
println!("Done blocking");
});
// Other tasks on this thread can't run during the sleep
// The runtime thread is blocked
// CORRECT: Use spawn_blocking for blocking operations
tokio::task::spawn_blocking(|| {
std::thread::sleep(std::time::Duration::from_secs(5));
println!("Done on blocking thread");
}).await.unwrap();
}Blocking in async code prevents other tasks from running on that thread.
#[tokio::main]
async fn main() {
// tokio::spawn: async code that yields
tokio::spawn(async {
// Non-blocking async operations
let data = tokio::fs::read_to_string("file.txt").await.unwrap();
println!("Read {} bytes", data.len());
});
// spawn_blocking: sync code that blocks
tokio::task::spawn_blocking(|| {
// Blocking operations
let contents = std::fs::read_to_string("file.txt").unwrap();
println!("Read {} bytes", contents.len());
});
}Use spawn for async, spawn_blocking for blocking sync code.
#[tokio::main]
async fn main() {
// CPU-intensive work should not block the runtime
let handle = tokio::task::spawn_blocking(|| {
let mut sum = 0u64;
for i in 0..10_000_000 {
sum = sum.wrapping_add(i);
}
sum
});
// Runtime stays responsive while calculation runs
let result = handle.await.unwrap();
println!("Sum: {}", result);
}CPU-bound work blocks the threadāuse spawn_blocking to isolate it.
#[tokio::main]
async fn main() -> std::io::Result<()> {
// Async I/O: use tokio::spawn with async functions
tokio::spawn(async {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut file = tokio::fs::File::open("async_file.txt").await?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?;
println!("Read {} bytes async", buffer.len());
Ok::<_, std::io::Error>(())
});
// Sync I/O: must use spawn_blocking
tokio::task::spawn_blocking(|| {
use std::io::Read;
let mut file = std::fs::File::open("sync_file.txt")?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
println!("Read {} bytes sync", buffer.len());
Ok::<_, std::io::Error>(())
}).await.unwrap()?;
Ok(())
}Use async I/O with tokio::spawn, sync I/O with spawn_blocking.
#[tokio::main]
async fn main() {
// tokio::spawn: runs on async runtime threads (few threads)
tokio::spawn(async {
println!("Async thread: {:?}", std::thread::current().id());
});
// spawn_blocking: runs on blocking thread pool (many threads)
tokio::task::spawn_blocking(|| {
println!("Blocking thread: {:?}", std::thread::current().id());
}).await.unwrap();
// Runtime has limited async threads (typically # cores)
// Blocking pool can grow to 512 threads by default
}The async runtime uses few threads; blocking pool can have many.
#[tokio::main]
async fn main() {
// Blocking pool defaults to max 512 threads
// Each blocks for up to timeout before thread is released
// Spawn many blocking tasks
for i in 0..100 {
tokio::task::spawn_blocking(move || {
std::thread::sleep(std::time::Duration::from_millis(100));
println!("Task {} done", i);
});
}
// All 100 can run concurrently on blocking pool
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}The blocking pool is designed for concurrent blocking operations.
#[tokio::main]
async fn main() {
// Both return JoinHandle
let async_handle = tokio::spawn(async { 1 });
let blocking_handle = tokio::task::spawn_blocking(|| 2);
// Both can be awaited for result
let a = async_handle.await.unwrap();
let b = blocking_handle.await.unwrap();
println!("Results: {}, {}", a, b);
// Detached tasks: both continue running if handle dropped
tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("Detached async task completed");
});
tokio::task::spawn_blocking(|| {
std::thread::sleep(std::time::Duration::from_millis(100));
println!("Detached blocking task completed");
});
}Both return JoinHandle and can be detached by dropping the handle.
#[tokio::main]
async fn main() {
// Both can panic
let async_result = tokio::spawn(async {
panic!("Async panic!");
}).await;
let blocking_result = tokio::task::spawn_blocking(|| {
panic!("Blocking panic!");
}).await;
// Both return Err(JoinError) on panic
assert!(async_result.is_err());
assert!(blocking_result.is_err());
}Both propagate panics through the JoinHandle.
#[tokio::main]
async fn main() {
// tokio::spawn: async block, return type must be Send + 'static
let handle = tokio::spawn(async {
42u32 // u32: Send + 'static
});
// spawn_blocking: sync closure, return type must be Send + 'static
let handle = tokio::task::spawn_blocking(|| {
42u32 // Same constraints
});
// Both require Send because tasks can move between threads
}Both require Send because tasks run on thread pools.
#[tokio::main]
async fn main() {
// tokio::spawn requires Send
// For non-Send types, use tokio::task::spawn_local
// This won't compile:
// let rc = std::rc::Rc::new(42);
// tokio::spawn(async move { *rc }); // Rc is not Send
// Use spawn_local for non-Send (requires current_thread runtime)
// Note: spawn_local requires #[tokio::main(flavor = "current_thread")]
}
// For non-Send in multi-threaded runtime, use Arc instead
#[tokio::main]
async fn main2() {
use std::sync::Arc;
let arc = Arc::new(42);
tokio::spawn(async move {
println!("{}", *arc);
});
}tokio::spawn requires Send; for non-Send types, use alternatives.
#[tokio::main]
async fn main() {
// tokio::spawn is very cheap
// Task creation: ~few nanoseconds
for _ in 0..1_000_000 {
tokio::spawn(async {
// Minimal overhead
});
}
// spawn_blocking is more expensive
// Thread creation/assignment: ~microseconds
// Only use when truly necessary
tokio::task::spawn_blocking(|| {
// Worth the overhead for blocking operations
});
}tokio::spawn is lightweight; spawn_blocking has more overhead.
#[tokio::main]
async fn main() {
// Default: blocking pool max 512 threads
// Each thread kept alive for ~10s after last task
// Configure with runtime builder:
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4) // Async runtime threads
.max_blocking_threads(32) // Blocking pool size
.build()
.unwrap();
runtime.block_on(async {
tokio::task::spawn_blocking(|| {
// Uses configured blocking pool
});
});
}Configure blocking pool size based on expected blocking workload.
#[tokio::main]
async fn main() {
// MISTAKE 1: Blocking in async code
tokio::spawn(async {
// BAD: Blocks the runtime
std::thread::sleep(Duration::from_secs(1));
});
// CORRECT: Use tokio's async sleep
tokio::spawn(async {
tokio::time::sleep(Duration::from_secs(1)).await;
});
// MISTAKE 2: Using spawn_blocking for async operations
tokio::task::spawn_blocking(|| {
// BAD: Wastes blocking thread for async work
// Can't use .await inside sync closure anyway
});
// MISTAKE 3: Not awaiting spawn_blocking handle
tokio::task::spawn_blocking(|| {
// Task runs, but program might exit before completion
});
// Should await or use proper synchronization
}Common pitfalls include blocking in async code and misusing spawn_blocking.
#[tokio::main]
async fn main() {
// FFI calls that block must use spawn_blocking
tokio::task::spawn_blocking(|| {
// Hypothetical blocking FFI call
// external_library::blocking_call();
});
// Libraries without async support
tokio::task::spawn_blocking(|| {
// Using sync-only libraries
let client = reqwest::blocking::Client::new();
let resp = client.get("https://example.com").send().unwrap();
println!("Status: {}", resp.status());
}).await.unwrap();
}Use spawn_blocking for FFI and libraries without async support.
#[tokio::main]
async fn main() {
// Pattern: async I/O, then CPU work, then async I/O
let data = tokio::fs::read_to_string("input.txt").await.unwrap();
let result = tokio::task::spawn_blocking(move || {
// CPU-intensive processing
data.lines().filter(|l| l.len() > 10).count()
}).await.unwrap();
// Back to async for I/O
tokio::fs::write("output.txt", format!("Long lines: {}", result))
.await
.unwrap();
}Combine both for I/O-bound and CPU-bound phases.
| Feature | tokio::spawn | spawn_blocking |
|---------|---------------|------------------|
| Runs on | Async runtime threads | Blocking thread pool |
| Argument | Async block | Sync closure |
| Blocks runtime | No (yields) | No (isolated) |
| Overhead | Very low | Moderate |
| Use case | Async code | Blocking sync code |
| Thread count | ~CPU cores | Up to 512 |
| Can await | Yes | Only in outer scope |
The choice between tokio::spawn and spawn_blocking depends on whether the work yields or blocks:
tokio::spawn schedules async tasks that cooperatively yield control back to the runtime. Use it for async I/O, timers, and any code that uses .await. The runtime can multiplex thousands of tasks on few threads because tasks yield at await points.
spawn_blocking offloads blocking work to a dedicated thread pool where blocking doesn't impact the async runtime. Use it for CPU-intensive computation, synchronous I/O, FFI calls, and any operation that would block a thread without yielding.
Key insight: The async runtime achieves high concurrency through cooperative multitaskingātasks must yield. When code blocks instead of yielding, it prevents other tasks from running on that thread. spawn_blocking isolates blocking work on threads dedicated to that purpose, keeping the async runtime responsive. The overhead of spawn_blocking (thread pool management) is acceptable when the alternative is blocking the runtime, but should not be used for truly async operations that can yield.