009 - Async Concurrency with Tokio
Learning Rust as a Pythonista: Async Concurrency with Tokio
This is part 2 of concurrency. In part 1, we used threads/channels/shared state. Here we focus on asynchronous I/O-style concurrency with async/await and Tokio.
Why this matters for Python developers
If you already use asyncio, this will feel familiar:
async def↔async fnawait↔await- event loop/runtime ↔ async runtime (
tokio)
The big difference is that Rust still enforces strong type and memory guarantees at compile time.
Learning goals
By the end of this lesson, you should be able to:
- Write an async Rust function and await it.
- Spawn concurrent async tasks with
tokio::spawn. - Add minimal Tokio setup in
Cargo.toml.
Concepts in 5 minutes
async fnreturns aFuture.- A runtime (Tokio) drives futures to completion.
- Use
tokio::spawnfor concurrent tasks. - Use
join!/awaitto synchronize task completion.
Python baseline snippet
import asyncio
async def fetch(name, delay):
await asyncio.sleep(delay)
return f"{name} done"
async def main():
a = asyncio.create_task(fetch("A", 1))
b = asyncio.create_task(fetch("B", 1))
print(await a)
print(await b)
asyncio.run(main())
Rust setup (Cargo.toml)
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
Rust equivalent snippets
1) Basic async function
use tokio::time::{sleep, Duration};
async fn fetch(name: &str, delay_ms: u64) -> String {
sleep(Duration::from_millis(delay_ms)).await;
format!("{} done", name)
}
2) Spawn tasks
let a = tokio::spawn(async { fetch("A", 500).await });
let b = tokio::spawn(async { fetch("B", 500).await });
println!("{}", a.await.unwrap());
println!("{}", b.await.unwrap());
One runnable end-to-end example
use tokio::time::{sleep, Duration};
async fn fetch(name: &str, delay_ms: u64) -> String {
sleep(Duration::from_millis(delay_ms)).await;
format!("{} done", name)
}
#[tokio::main]
async fn main() {
let tasks = vec![
tokio::spawn(async { fetch("profile", 300).await }),
tokio::spawn(async { fetch("orders", 500).await }),
tokio::spawn(async { fetch("recommendations", 200).await }),
];
for task in tasks {
match task.await {
Ok(value) => println!("{}", value),
Err(e) => eprintln!("task failed: {}", e),
}
}
println!("all async tasks completed");
}
Common mistakes
- Forgetting to add Tokio dependency/features.
- Calling async functions without
await. - Doing heavy CPU work directly in async tasks (use threads or
spawn_blockinginstead).
Quick practice
- Add a fourth task and print completion order.
- Change delays and verify faster tasks complete first.
Recap
Use async for high-concurrency I/O workflows; use threads for CPU-heavy parallel work. Rust supports both models cleanly and safely.
Next, we continue with advanced pattern matching.
Join the Journey Ahead!
If you're eager to continue this learning journey and stay updated with the latest insights, consider subscribing. By joining our mailing list, you'll receive notifications about new articles, tips, and resources to help you seamlessly pick up Rust by leveraging your Python skills.
Other articles in the series
- 000 - Learning Rust as a Pythonista: A Suggested Path
- 001 - Your First Rust Program
- 002 - Basic Syntax and Structure
- 003 - Ownership, Borrowing, and Lifetimes
- 004 - Error Handling
- 005 - Structs and Enums
- 006 - Iterators and Closures
- 007 - Traits vs Duck Typing and Protocols
- 008 - Concurrency in Rust for Python Developers
- 010 - Pattern Matching in Rust
- 011 - Macros in Rust