Introduction
Tokio is Rust’s de facto standard async runtime, enabling millions of concurrent tasks on a single machine. Understanding Tokio is essential for building high-performance network services.
This guide covers Tokio architecture, async patterns, and real-world networking examples.
Why Async?
Concurrency Models Comparison
Threads vs Async in Rust:
Model Memory/conn Context Switches Latency Max Connections
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Thread pool ~2MB 1000s/sec high 10k-100k
Async (Tokio) ~50bytes 100-1000/sec low 1M+
Example: 10,000 concurrent connections
- Thread pool: 20GB RAM
- Tokio: 500MB RAM (40x savings)
When to Use Async
USE ASYNC:
โ
Network I/O (HTTP, databases, WebSockets)
โ
1000s of concurrent connections
โ
Want low memory footprint
โ
Latency-sensitive applications
USE THREADS:
โ
CPU-bound work (calculations, compression)
โ
Few concurrent tasks
โ
Need true parallelism on multi-core
Tokio Runtime Overview
What is Tokio?
Tokio = Async Runtime
โโโ Work-stealing scheduler
โโโ I/O multiplexing (epoll/kqueue)
โโโ Timer management
โโโ Networking libraries
โโโ Synchronization primitives
Architecture Diagram
Tokio Runtime
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Scheduler (thread pool) โ
โ โโโโโโโโฌโโโโโโโฌโโโโโโโฌโโโโโโโ โ
โ โ W1 โ W2 โ W3 โ W4 โ โ
โ โโโโโโโโดโโโโโโโดโโโโโโโดโโโโโโโ โ
โ (work stealing) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ I/O Reactor (epoll/kqueue) โ
โ Events from socket/file descriptors โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Timer Wheel + Timers โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Getting Started with Tokio
Minimal Setup
[dependencies]
tokio = { version = "1.35", features = ["full"] }
Basic Async Function
use tokio::time::sleep;
use std::time::Duration;
#[tokio::main]
async fn main() {
println!("Start");
sleep(Duration::from_secs(1)).await;
println!("After 1 second");
}
The #[tokio::main] Macro
The macro expands to:
fn main() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
println!("In async block");
});
}
Async/Await Syntax
Futures and Await
use tokio::time::sleep;
use std::time::Duration;
async fn slow_operation() -> String {
sleep(Duration::from_secs(1)).await;
"result".to_string()
}
#[tokio::main]
async fn main() {
let result = slow_operation().await;
println!("{}", result);
}
Spawning Tasks
#[tokio::main]
async fn main() {
// Spawn background task
tokio::spawn(async {
for i in 0..5 {
println!("Background task: {}", i);
tokio::time::sleep(Duration::from_millis(100)).await;
}
});
// Main task continues
for i in 0..3 {
println!("Main task: {}", i);
tokio::time::sleep(Duration::from_millis(200)).await;
}
// Wait for background task
tokio::time::sleep(Duration::from_millis(1000)).await;
}
// Output:
// Main task: 0
// Background task: 0
// Background task: 1
// Main task: 1
// ...
Joining Multiple Tasks
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
"result 1"
});
let task2 = tokio::spawn(async {
sleep(Duration::from_secs(2)).await;
"result 2"
});
let (r1, r2) = tokio::join!(task1, task2);
println!("{:?}, {:?}", r1, r2);
}
Tokio Networking
TCP Server
use tokio::net::TcpListener;
use tokio::io::{AsyncWriteExt, AsyncReadExt};
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8000")
.await
.expect("Failed to bind");
loop {
let (mut socket, addr) = listener
.accept()
.await
.expect("Failed to accept");
// Spawn task for each connection
tokio::spawn(async move {
let mut buf = vec![0; 1024];
loop {
let n = socket.read(&mut buf).await.unwrap();
if n == 0 { return; }
socket.write_all(&buf[..n]).await.unwrap();
}
});
}
}
TCP Client
use tokio::net::TcpStream;
use tokio::io::{AsyncWriteExt, AsyncReadExt};
#[tokio::main]
async fn main() {
let mut stream = TcpStream::connect("127.0.0.1:8000")
.await
.expect("Failed to connect");
// Send data
stream.write_all(b"Hello").await.unwrap();
// Read response
let mut buf = vec![0; 1024];
let n = stream.read(&mut buf).await.unwrap();
println!("Received: {}", String::from_utf8_lossy(&buf[..n]));
}
Concurrency Primitives
Mutex (Lock)
use tokio::sync::Mutex;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0));
let mut tasks = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
tasks.push(tokio::spawn(async move {
for _ in 0..100 {
let mut num = c.lock().await;
*num += 1;
}
}));
}
for task in tasks {
task.await.unwrap();
}
println!("Counter: {}", *counter.lock().await);
}
Channel (Communication)
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(100);
// Producer task
tokio::spawn(async move {
for i in 0..10 {
tx.send(i).await.unwrap();
}
});
// Consumer task
while let Some(msg) = rx.recv().await {
println!("Received: {}", msg);
}
}
Broadcast (Pub/Sub)
use tokio::sync::broadcast;
#[tokio::main]
async fn main() {
let (tx, _) = broadcast::channel(10);
// Publisher
for i in 0..5 {
tx.send(i).unwrap();
}
// Multiple subscribers
let mut rx1 = tx.subscribe();
let mut rx2 = tx.subscribe();
tokio::spawn(async move {
while let Ok(msg) = rx1.recv().await {
println!("Sub1: {}", msg);
}
});
tokio::spawn(async move {
while let Ok(msg) = rx2.recv().await {
println!("Sub2: {}", msg);
}
});
tokio::time::sleep(Duration::from_millis(100)).await;
}
Real-World Example: HTTP Echo Server
use tokio::net::TcpListener;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8000")
.await
.unwrap();
println!("Listening on 127.0.0.1:8000");
loop {
let (socket, addr) = listener.accept().await.unwrap();
println!("New connection: {}", addr);
tokio::spawn(async move {
handle_connection(socket).await;
});
}
}
async fn handle_connection(socket: tokio::net::TcpStream) {
let (reader, mut writer) = socket.into_split();
let mut lines = BufReader::new(reader).lines();
let mut headers = vec![];
while let Ok(Some(line)) = lines.next_line().await {
if line.is_empty() { break; }
headers.push(line);
}
if let Some(request_line) = headers.first() {
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello"
);
writer.write_all(response.as_bytes()).await.ok();
}
}
Performance Characteristics
Benchmark Results (1M concurrent connections)
Metric Tokio
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Memory per connection ~500 bytes
Task switch latency <1 microsecond
Context switches/sec 10,000-50,000
P99 latency (echo) < 100 microseconds
Throughput (echo) > 1M req/sec
CPU cores needed 4-8
Scaling Patterns
Connections Threads Tokio (CPU) Memory
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1K 1 1% 20MB
10K 10 5% 200MB
100K 100 15% 2GB
1M 1000 30-50% 20GB <- Not practical
1M (async) 4 40-60% 500MB <- Tokio excels
Debugging Async Code
Logs and Tracing
use tracing::{info, error, span, Level};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let span = span!(Level::INFO, "process");
let _enter = span.enter();
info!("Starting operation");
match operation().await {
Ok(result) => info!("Success: {}", result),
Err(e) => error!("Failed: {}", e),
}
}
async fn operation() -> Result<String, String> {
Ok("Done".to_string())
}
Timeouts
use tokio::time::timeout;
use std::time::Duration;
#[tokio::main]
async fn main() {
let result = timeout(
Duration::from_secs(5),
slow_operation()
).await;
match result {
Ok(Ok(data)) => println!("Success: {}", data),
Ok(Err(e)) => println!("Operation error: {}", e),
Err(_) => println!("Timeout!"),
}
}
async fn slow_operation() -> Result<String, String> {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok("Done".to_string())
}
Glossary
- Async Runtime: System that schedules and executes async tasks
- Future: Represents a value that may not be ready yet
- Await: Syntax to wait for a Future to complete
- Executor: Component that runs async tasks
- Work Stealing: Scheduler technique for load balancing
- Reactor: I/O multiplexing system (epoll/kqueue)
Comments