Advanced Rust Patterns: Moderate to Difficult Use Cases
Research Date: 2025-12-01 Focus: Ownership, Traits, Async, Error Handling, Type-Safe Design Complexity Level: Moderate to Difficult
Table of Contents
1. Ownership & Borrowing Mastery
1.1 Advanced Lifetime Patterns
Pattern Name: Explicit Lifetime Annotations with Multiple References Complexity: ⭐⭐⭐ Moderate Safety Guarantees: Prevents dangling references, ensures memory safety
Problem Solved
When a method receives two references with potentially different lifetimes and returns a reference, the compiler needs explicit lifetime annotations to determine which input lifetime the output is tied to.
Code Example
struct ContentManager {
content: String,
}
impl ContentManager {
// Explicit lifetime annotation required
fn get_content_or_default<'a, 'b>(
&'a self,
default: &'b str,
) -> &'a str
where
'b: 'a, // 'b must outlive 'a
{
if self.content.is_empty() {
// Cannot return default here due to lifetime mismatch
// This forces us to return from self
&self.content
} else {
&self.content
}
}
// Better pattern: return owned when necessary
fn get_content_or_default_owned(&self, default: &str) -> String {
if self.content.is_empty() {
default.to_string()
} else {
self.content.clone()
}
}
}
// Advanced: Generic lifetime bounds with traits
trait Cache<'a, T> {
fn get(&'a self, key: &str) -> Option<&'a T>;
}
struct MemoryCache<T> {
data: std::collections::HashMap<String, T>,
}
impl<'a, T> Cache<'a, T> for MemoryCache<T> {
fn get(&'a self, key: &str) -> Option<&'a T> {
self.data.get(key)
}
}
Lifetime Elision Rules
Rule 1: Each reference parameter gets its own lifetime
fn foo(x: &i32, y: &i32) // becomes
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
Rule 2: If there is one input lifetime, it's assigned to all outputs
fn process(input: &str) -> &str // becomes
fn process<'a>(input: &'a str) -> &'a str
Rule 3: If there are multiple input lifetimes but one is &self, the output gets self's lifetime
impl Foo {
fn method(&self, other: &str) -> &str // becomes
fn method<'a, 'b>(&'a self, other: &'b str) -> &'a str
}
When to Use:
- When returning references from structs with multiple lifetime dependencies
- When elision rules cannot infer the correct lifetime relationship
- When building generic cache or reference-holding data structures
Borrow Checker Issues Solved:
- "borrowed value does not live long enough"
- "cannot infer an appropriate lifetime"
- "lifetime may not live long enough"
Performance: Zero-cost abstraction - lifetimes are compile-time only
1.2 Interior Mutability Patterns
Pattern Name: RefCell and Cell for Single-Threaded Interior Mutability Complexity: ⭐⭐⭐ Moderate Safety Guarantees: Runtime borrow checking, single-threaded safety
Problem Solved
Allows mutation through shared references when the compiler cannot prove safety at compile time, enabling patterns like multiple owners with mutation or circular references.
Code Example
use std::cell::{Cell, RefCell};
use std::rc::Rc;
// Example 1: Graph with RefCell for interior mutability
#[derive(Debug)]
struct Node {
value: i32,
neighbors: RefCell<Vec<Rc<Node>>>,
}
impl Node {
fn new(value: i32) -> Rc<Self> {
Rc::new(Node {
value,
neighbors: RefCell::new(Vec::new()),
})
}
fn add_neighbor(&self, neighbor: Rc<Node>) {
// Mutate through shared reference
self.neighbors.borrow_mut().push(neighbor);
}
fn neighbors_count(&self) -> usize {
self.neighbors.borrow().len()
}
}
// Example 2: Mock object pattern with RefCell
struct MockDatabase {
call_count: RefCell<usize>,
responses: RefCell<Vec<String>>,
}
impl MockDatabase {
fn new() -> Self {
MockDatabase {
call_count: RefCell::new(0),
responses: RefCell::new(Vec::new()),
}
}
fn query(&self, _sql: &str) -> String {
// Mutate call count through shared reference
*self.call_count.borrow_mut() += 1;
// Return mock response
self.responses
.borrow_mut()
.pop()
.unwrap_or_else(|| "default".to_string())
}
fn times_called(&self) -> usize {
*self.call_count.borrow()
}
}
// Example 3: Cell for Copy types
struct Metrics {
request_count: Cell<u64>,
error_count: Cell<u64>,
}
impl Metrics {
fn new() -> Self {
Metrics {
request_count: Cell::new(0),
error_count: Cell::new(0),
}
}
fn record_request(&self) {
// No borrow_mut needed for Cell
self.request_count.set(self.request_count.get() + 1);
}
fn record_error(&self) {
self.error_count.set(self.error_count.get() + 1);
}
}
RefCell vs Cell vs Mutex
| Type | Thread Safety | Borrow Check | Copy Types Only | Runtime Cost |
|---|---|---|---|---|
Cell<T> |
❌ No | No borrowing | ✅ Yes | Lowest |
RefCell<T> |
❌ No | ✅ Runtime | ❌ No | Low (runtime checks) |
Mutex<T> |
✅ Yes | ✅ Runtime | ❌ No | Higher (locking) |
When to Use:
- Cell: For Copy types (integers, booleans) that need mutation through shared references
- RefCell: For non-Copy types in single-threaded scenarios (UI, tests, graphs)
- Rc<RefCell>: Multiple ownership + interior mutability (graphs, trees)
Borrow Checker Issues Solved:
- "cannot borrow as mutable" when only shared reference available
- Circular reference patterns (with Weak to prevent leaks)
- Mock objects in tests that need mutation
Performance:
- Cell: Zero overhead
- RefCell: Small runtime cost for borrow tracking
- Panics at runtime if borrow rules violated
Common Pitfall:
// DANGER: This will panic!
let data = RefCell::new(vec![1, 2, 3]);
let borrow1 = data.borrow_mut(); // OK
let borrow2 = data.borrow_mut(); // PANIC: already borrowed
1.3 Smart Pointer Patterns: Arc<Mutex> vs Arc<RwLock>
Pattern Name: Thread-Safe Shared Ownership with Controlled Mutation Complexity: ⭐⭐⭐⭐ Difficult Safety Guarantees: Thread-safe sharing, prevents data races
Problem Solved
Enables multiple threads to share and mutate data safely, choosing the right synchronization primitive based on read/write ratio.
Code Example
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::Duration;
// Example 1: Arc<Mutex<T>> for balanced read/write
#[derive(Clone)]
struct Counter {
value: Arc<Mutex<u64>>,
}
impl Counter {
fn new() -> Self {
Counter {
value: Arc::new(Mutex::new(0)),
}
}
fn increment(&self) {
let mut val = self.value.lock().unwrap();
*val += 1;
}
fn get(&self) -> u64 {
*self.value.lock().unwrap()
}
}
// Example 2: Arc<RwLock<T>> for read-heavy workloads
#[derive(Clone)]
struct Cache {
data: Arc<RwLock<std::collections::HashMap<String, String>>>,
}
impl Cache {
fn new() -> Self {
Cache {
data: Arc::new(RwLock::new(std::collections::HashMap::new())),
}
}
// Many threads can read simultaneously
fn get(&self, key: &str) -> Option<String> {
let read_guard = self.data.read().unwrap();
read_guard.get(key).cloned()
}
// Only one thread can write
fn set(&self, key: String, value: String) {
let mut write_guard = self.data.write().unwrap();
write_guard.insert(key, value);
}
}
// Example 3: Practical multi-threaded server pattern
struct Server {
connections: Arc<RwLock<Vec<String>>>,
stats: Arc<Mutex<ServerStats>>,
}
struct ServerStats {
requests: u64,
errors: u64,
}
impl Server {
fn new() -> Self {
Server {
connections: Arc::new(RwLock::new(Vec::new())),
stats: Arc::new(Mutex::new(ServerStats {
requests: 0,
errors: 0,
})),
}
}
fn add_connection(&self, conn: String) {
let mut conns = self.connections.write().unwrap();
conns.push(conn);
}
fn get_connection_count(&self) -> usize {
// Read lock allows multiple concurrent reads
self.connections.read().unwrap().len()
}
fn record_request(&self) {
let mut stats = self.stats.lock().unwrap();
stats.requests += 1;
}
fn spawn_workers(&self, count: usize) {
for i in 0..count {
let stats = Arc::clone(&self.stats);
thread::spawn(move || {
for _ in 0..100 {
let mut s = stats.lock().unwrap();
s.requests += 1;
thread::sleep(Duration::from_millis(10));
}
});
}
}
}
Performance Characteristics
Mutex:
- Consistent performance for reads and writes
- Lower overhead for single operation
- Better for write-heavy or balanced workloads
- No writer starvation
RwLock:
- Excellent for read-heavy workloads (90%+ reads)
- Higher overhead per operation
- Multiple concurrent readers
- Can suffer from writer starvation
- Performance degrades significantly with many writes
Benchmark Results (typical):
Workload: 90% reads, 10% writes
- Arc<Mutex<T>>: ~500ns per operation
- Arc<RwLock<T>>: ~100ns per operation (5x faster)
Workload: 50% reads, 50% writes
- Arc<Mutex<T>>: ~500ns per operation
- Arc<RwLock<T>>: ~600ns per operation (slower)
When to Use:
Arc<Mutex>:
- Default choice for shared mutable state
- Balanced read/write ratio (>20% writes)
- Simple shared counters or flags
- When in doubt, start here
Arc<RwLock>:
- Read-heavy workloads (>80% reads)
- Shared caches
- Configuration data that rarely changes
- Large data structures where read operations are expensive
Arc<Atomic*>:
- Simple atomic types (integers, booleans)
- Lock-free counters
- Highest performance for simple values
Borrow Checker Issues Solved:
- "cannot move out of shared reference"
- "cannot borrow as mutable in multiple threads"
- Data race prevention at compile time
Common Pitfalls:
// DEADLOCK: Don't lock multiple mutexes in different orders
let lock1 = mutex1.lock().unwrap();
let lock2 = mutex2.lock().unwrap(); // Thread A
// vs
let lock2 = mutex2.lock().unwrap();
let lock1 = mutex1.lock().unwrap(); // Thread B - DEADLOCK!
// POISON: Handle panics in locked sections
match mutex.lock() {
Ok(guard) => { /* use guard */ },
Err(poisoned) => {
// Mutex poisoned due to panic while locked
let guard = poisoned.into_inner(); // Recover if safe
}
}
2. Trait System
2.1 Associated Types vs Generic Parameters
Pattern Name: Type Projection vs Parameterization Complexity: ⭐⭐⭐⭐ Difficult Safety Guarantees: Type-safe abstraction, prevents ambiguous implementations
Problem Solved
Distinguishes between traits that should have multiple implementations per type (generics) versus single canonical implementation (associated types).
Code Example
// GENERIC TYPE PARAMETER: Multiple implementations possible
trait From<T> {
fn from(value: T) -> Self;
}
// Can implement From for many types
impl From<u8> for u32 {
fn from(value: u8) -> u32 {
value as u32
}
}
impl From<u16> for u32 {
fn from(value: u16) -> u32 {
value as u32
}
}
// ASSOCIATED TYPE: Only one implementation per type
trait Iterator {
type Item; // Associated type
fn next(&mut self) -> Option<Self::Item>;
}
// Counter can only iterate over one type
struct Counter {
count: u32,
}
impl Iterator for Counter {
type Item = u32; // Can't implement Iterator<String> for Counter
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
Some(self.count)
}
}
// Example: Choosing between approaches
// BAD: Iterator with generic would allow nonsense
trait BadIterator<T> {
fn next(&mut self) -> Option<T>;
}
// This compiles but makes no sense:
// impl BadIterator<String> for Counter { ... }
// impl BadIterator<u32> for Counter { ... }
// Now counter.next() is ambiguous!
// Example: Conversion trait with associated type
trait TryParse {
type Output;
type Error;
fn try_parse(s: &str) -> Result<Self::Output, Self::Error>;
}
struct JsonParser;
impl TryParse for JsonParser {
type Output = serde_json::Value;
type Error = serde_json::Error;
fn try_parse(s: &str) -> Result<Self::Output, Self::Error> {
serde_json::from_str(s)
}
}
// Advanced: Generic + associated type combo
trait Graph {
type Node;
type Edge;
fn neighbors(&self, node: &Self::Node) -> Vec<Self::Edge>;
}
trait GenericGraph<N, E> {
fn neighbors(&self, node: &N) -> Vec<E>;
}
// Associated types better: each graph has one node/edge type
// Generic would allow Graph<String, i32> AND Graph<u32, String> for same type
Decision Matrix
| Use Associated Types When | Use Generic Parameters When |
|---|---|
| ✅ Only one logical implementation per type | ✅ Multiple implementations make sense |
| ✅ Output type determined by input type | ✅ Caller chooses the type |
| ✅ Trait represents identity/capability | ✅ Trait represents conversion/transformation |
Example: Iterator::Item |
Example: From<T> |
Example: Deref::Target |
Example: Add<Rhs> |
When to Use:
- Associated Types: Iterator, Deref, Future (one obvious output type)
- Generic Parameters: From/Into, Add/Sub (multiple conversions make sense)
Borrow Checker Issues Solved:
- Prevents ambiguous type inference
- Enforces "one true implementation" at type level
- Cleaner APIs without turbofish (
::<>) syntax
Performance: Zero-cost - both approaches are compile-time only
2.2 Trait Objects (dyn Trait)
Pattern Name: Dynamic Dispatch with Type Erasure Complexity: ⭐⭐⭐⭐ Difficult Safety Guarantees: Object-safe traits, runtime polymorphism
Problem Solved
Enables heterogeneous collections and runtime polymorphism when concrete types aren't known at compile time.
Code Example
// Example 1: Object-safe trait for plugin system
trait Plugin: Send + Sync {
fn name(&self) -> &str;
fn execute(&self, input: &str) -> String;
}
struct UppercasePlugin;
impl Plugin for UppercasePlugin {
fn name(&self) -> &str { "uppercase" }
fn execute(&self, input: &str) -> String {
input.to_uppercase()
}
}
struct ReversePlugin;
impl Plugin for ReversePlugin {
fn name(&self) -> &str { "reverse" }
fn execute(&self, input: &str) -> String {
input.chars().rev().collect()
}
}
struct PluginManager {
plugins: Vec<Box<dyn Plugin>>,
}
impl PluginManager {
fn new() -> Self {
PluginManager { plugins: Vec::new() }
}
fn register(&mut self, plugin: Box<dyn Plugin>) {
self.plugins.push(plugin);
}
fn execute_all(&self, input: &str) -> Vec<String> {
self.plugins
.iter()
.map(|p| p.execute(input))
.collect()
}
}
// Example 2: GUI rendering with trait objects
trait Drawable {
fn draw(&self, canvas: &mut Canvas);
fn bounds(&self) -> Rectangle;
}
struct Circle { x: f64, y: f64, radius: f64 }
struct Rectangle { x: f64, y: f64, width: f64, height: f64 }
impl Drawable for Circle {
fn draw(&self, canvas: &mut Canvas) {
canvas.draw_circle(self.x, self.y, self.radius);
}
fn bounds(&self) -> Rectangle {
Rectangle {
x: self.x - self.radius,
y: self.y - self.radius,
width: self.radius * 2.0,
height: self.radius * 2.0,
}
}
}
struct Scene {
objects: Vec<Box<dyn Drawable>>,
}
impl Scene {
fn render(&self, canvas: &mut Canvas) {
for obj in &self.objects {
obj.draw(canvas); // Dynamic dispatch
}
}
}
// Example 3: Object-safe vs NOT object-safe
// OBJECT-SAFE ✅
trait Logger {
fn log(&self, message: &str);
}
// NOT OBJECT-SAFE ❌
trait BadTrait {
fn generic_method<T>(&self, value: T); // ❌ Generic methods
fn returns_self() -> Self; // ❌ Returns Self
fn sized_bound(&self) where Self: Sized; // ❌ Sized bound
}
// Can't do: Box<dyn BadTrait> - Compile error!
// Example 4: Trait object with lifetime bounds
trait Handler<'a> {
fn handle(&self, data: &'a str) -> String;
}
struct Processor {
handlers: Vec<Box<dyn for<'a> Handler<'a>>>, // Higher-rank trait bound
}
Object Safety Rules
A trait is object-safe if:
- ✅ No generic methods
- ✅ No associated functions that return
Self - ✅ No
Self: Sizedbounds - ✅ Methods use
&self,&mut self, orBox<Self>
Static vs Dynamic Dispatch:
// Static dispatch (monomorphization)
fn process_static<T: Plugin>(plugin: &T, input: &str) -> String {
plugin.execute(input) // Inlined, fast, larger binary
}
// Dynamic dispatch (vtable)
fn process_dynamic(plugin: &dyn Plugin, input: &str) -> String {
plugin.execute(input) // Pointer indirection, smaller binary
}
Performance Characteristics:
| Aspect | Static Dispatch | Dynamic Dispatch |
|---|---|---|
| Call overhead | None (inlined) | Vtable lookup |
| Binary size | Larger (copies) | Smaller |
| Flexibility | Compile-time only | Runtime composition |
| Speed | Faster (~2-3ns) | Slower (~5-7ns) |
When to Use:
- Heterogeneous collections (different types in same Vec)
- Plugin systems
- Dependency injection
- GUI frameworks
- When you can't know types at compile time
Alternatives:
- Enum dispatch (faster, but closed set of types)
- Generic parameters (fastest, but monomorphizes)
2.3 Newtype Pattern and Orphan Rule
Pattern Name: Type Wrapper for Trait Implementation Complexity: ⭐⭐⭐ Moderate Safety Guarantees: Orphan rule compliance, type safety
Problem Solved
Allows implementing foreign traits on foreign types by wrapping them in a local newtype, bypassing the orphan rule while maintaining type safety.
Code Example
use std::fmt;
// PROBLEM: Can't implement foreign trait on foreign type
// impl fmt::Display for Vec<i32> {} // ❌ Orphan rule violation!
// SOLUTION: Newtype pattern
struct MyVec(Vec<i32>);
impl fmt::Display for MyVec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[")?;
for (i, item) in self.0.iter().enumerate() {
if i > 0 { write!(f, ", ")?; }
write!(f, "{}", item)?;
}
write!(f, "]")
}
}
// Example 2: Deref for transparent access
use std::ops::Deref;
impl Deref for MyVec {
type Target = Vec<i32>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
// Now MyVec can use Vec methods!
let mut vec = MyVec(vec![1, 2, 3]);
vec.push(4); // Works via Deref coercion
// Example 3: Stronger type safety with newtype
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct UserId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct PostId(u64);
fn get_user(id: UserId) -> User { /* ... */ }
fn get_post(id: PostId) -> Post { /* ... */ }
// Type safety prevents bugs:
let user_id = UserId(42);
let post_id = PostId(42);
get_user(user_id); // ✅ OK
// get_user(post_id); // ❌ Compile error!
// Example 4: Zero-cost newtype with transparent representation
#[repr(transparent)]
struct Meters(f64);
impl Meters {
fn to_feet(&self) -> f64 {
self.0 * 3.28084
}
}
// Example 5: Implementing standard traits for ergonomics
impl From<Vec<i32>> for MyVec {
fn from(vec: Vec<i32>) -> Self {
MyVec(vec)
}
}
impl From<MyVec> for Vec<i32> {
fn from(wrapper: MyVec) -> Self {
wrapper.0
}
}
// Now conversions are easy
let vec = vec![1, 2, 3];
let my_vec: MyVec = vec.into();
let back: Vec<i32> = my_vec.into();
Orphan Rule Explained
Rule: You can implement a trait for a type only if either:
- The trait is defined in your crate, OR
- The type is defined in your crate
Examples:
// ✅ ALLOWED: Our trait, foreign type
trait MyTrait {}
impl MyTrait for Vec<i32> {}
// ✅ ALLOWED: Foreign trait, our type
struct MyType;
impl std::fmt::Display for MyType { /* ... */ }
// ❌ FORBIDDEN: Foreign trait, foreign type
// impl std::fmt::Display for Vec<i32> {} // Orphan rule!
// ✅ WORKAROUND: Newtype pattern
struct Wrapper(Vec<i32>);
impl std::fmt::Display for Wrapper { /* ... */ }
When to Use:
- Implementing foreign traits on foreign types
- Type-safe wrappers (UserId vs raw integers)
- Domain modeling with stronger types
- Adding trait implementations to external types
Performance:
#[repr(transparent)]guarantees zero-cost- Deref coercion provides transparent access
- No runtime overhead
2.4 Blanket Implementations
Pattern Name: Generic Trait Implementation for All Matching Types Complexity: ⭐⭐⭐⭐ Difficult Safety Guarantees: Coherence, automatic implementations
Problem Solved
Provides automatic trait implementations for any type that satisfies certain constraints, reducing code duplication and enabling powerful abstractions.
Code Example
// Example 1: Classic From/Into blanket implementation
// From standard library:
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
}
// This means: implement From, get Into for free!
struct Meters(f64);
struct Feet(f64);
impl From<Meters> for Feet {
fn from(m: Meters) -> Feet {
Feet(m.0 * 3.28084)
}
}
// Automatically available:
let meters = Meters(10.0);
let feet: Feet = meters.into(); // Works via blanket impl!
// Example 2: Custom blanket implementation
trait ToJson {
fn to_json(&self) -> String;
}
// Blanket impl for all types that implement Display
impl<T: std::fmt::Display> ToJson for T {
fn to_json(&self) -> String {
format!("\"{}\"", self)
}
}
// Now all Display types get to_json for free
let num = 42;
println!("{}", num.to_json()); // "42"
// Example 3: Reference blanket implementations
trait Processable {
fn process(&self) -> String;
}
struct Data(String);
impl Processable for Data {
fn process(&self) -> String {
self.0.to_uppercase()
}
}
// Blanket impl for references
impl<T: Processable> Processable for &T {
fn process(&self) -> String {
(*self).process()
}
}
// Blanket impl for boxes
impl<T: Processable> Processable for Box<T> {
fn process(&self) -> String {
(**self).process()
}
}
// Example 4: Advanced - conditional blanket implementation
trait Summarize {
fn summarize(&self) -> String;
}
// Only implement for types that are Debug + Clone
impl<T> Summarize for T
where
T: std::fmt::Debug + Clone,
{
fn summarize(&self) -> String {
format!("{:?}", self)
}
}
// Example 5: Coherence rules prevent conflicts
trait MyTrait {
fn do_thing(&self);
}
// ❌ CONFLICT: Can't have overlapping blanket impls
// impl<T> MyTrait for T { ... }
// impl<T: Clone> MyTrait for T { ... } // Compile error!
// ✅ OK: Non-overlapping implementations
impl MyTrait for i32 { /* ... */ }
impl MyTrait for String { /* ... */ }
Coherence Rules
Rust enforces coherence: For any trait + type combination, there must be at most one implementation.
Key Rules:
- No two blanket implementations may overlap
- Specific implementations override blanket implementations
- Foreign trait + foreign type = can't implement (orphan rule)
Common Blanket Implementation Patterns:
// Pattern 1: Implement for references
impl<T: MyTrait + ?Sized> MyTrait for &T { /* ... */ }
impl<T: MyTrait + ?Sized> MyTrait for &mut T { /* ... */ }
impl<T: MyTrait + ?Sized> MyTrait for Box<T> { /* ... */ }
// Pattern 2: Error conversion
impl<T, E> From<E> for Result<T, E> {
fn from(err: E) -> Self {
Err(err)
}
}
// Pattern 3: Optional conversion
impl<T> From<T> for Option<T> {
fn from(val: T) -> Self {
Some(val)
}
}
When to Use:
- Automatic trait propagation (From → Into)
- Implementing for reference types (&T, &mut T, Box)
- Generic conversions and utilities
- Framework-level abstractions
Performance: Zero-cost - resolved at compile time
3. Async/Concurrency
3.1 Tokio Runtime Patterns
Pattern Name: Async Runtime Management and Task Spawning Complexity: ⭐⭐⭐ Moderate Safety Guarantees: Send + Sync enforcement, structured concurrency
Problem Solved
Provides efficient cooperative multitasking for I/O-bound operations with proper async/await patterns and runtime management.
Code Example
use tokio::runtime::Runtime;
use tokio::task;
use std::time::Duration;
// Example 1: Different runtime configurations
fn main() {
// Multi-threaded runtime (default)
let rt = Runtime::new().unwrap();
rt.block_on(async {
println!("Running on multi-threaded runtime");
});
// Current thread runtime (single-threaded)
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
println!("Single-threaded runtime");
});
// Custom configured runtime
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.thread_name("my-pool")
.thread_stack_size(3 * 1024 * 1024)
.enable_all()
.build()
.unwrap();
}
// Example 2: Task spawning patterns
#[tokio::main]
async fn main() {
// Spawn tasks with JoinHandle
let handle1 = task::spawn(async {
tokio::time::sleep(Duration::from_secs(1)).await;
"task 1 complete"
});
let handle2 = task::spawn(async {
tokio::time::sleep(Duration::from_secs(2)).await;
"task 2 complete"
});
// Wait for both
let (result1, result2) = tokio::join!(handle1, handle2);
println!("{:?}, {:?}", result1, result2);
}
// Example 3: Blocking task pattern
#[tokio::main]
async fn main() {
// DON'T block the async runtime
// ❌ task::spawn(async { std::thread::sleep(...) }); // Blocks thread!
// ✅ Use spawn_blocking for CPU-intensive work
let result = task::spawn_blocking(|| {
// This runs on a dedicated thread pool
expensive_cpu_work()
}).await.unwrap();
println!("Result: {}", result);
}
fn expensive_cpu_work() -> u64 {
(0..1_000_000).sum()
}
// Example 4: Practical async server pattern
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Server listening on port 8080");
loop {
let (mut socket, addr) = listener.accept().await?;
// Spawn a task per connection
task::spawn(async move {
let mut buf = [0; 1024];
match socket.read(&mut buf).await {
Ok(n) if n == 0 => return, // Connection closed
Ok(n) => {
// Echo back
if let Err(e) = socket.write_all(&buf[0..n]).await {
eprintln!("Failed to write to {}: {}", addr, e);
}
}
Err(e) => {
eprintln!("Failed to read from {}: {}", addr, e);
}
}
});
}
}
// Example 5: JoinSet for dynamic task collection
use tokio::task::JoinSet;
#[tokio::main]
async fn main() {
let mut set = JoinSet::new();
// Dynamically spawn tasks
for i in 0..10 {
set.spawn(async move {
tokio::time::sleep(Duration::from_millis(i * 100)).await;
i * 2
});
}
// Collect results as they complete
while let Some(res) = set.join_next().await {
match res {
Ok(value) => println!("Task completed: {}", value),
Err(e) => eprintln!("Task failed: {}", e),
}
}
}
Runtime Selection Guide
| Runtime Type | Use Case | Thread Count | Overhead |
|---|---|---|---|
#[tokio::main] |
Default choice | CPU count | Medium |
new_multi_thread() |
High concurrency | Configurable | Medium |
new_current_thread() |
Tests, simple apps | 1 | Low |
spawn_blocking() |
CPU-bound work | Dedicated pool | Higher |
When to Use:
- Multi-threaded: I/O-bound servers, network apps
- Current thread: Tests, single-threaded environments, WASM
- spawn_blocking: File I/O, CPU-intensive computation, synchronous APIs
Performance Characteristics:
- Task spawning: ~1-2μs overhead
- Context switching: ~50-100ns
- Much lighter than OS threads (can spawn 100k+ tasks)
3.2 Graceful Shutdown Patterns
Pattern Name: Coordinated Async Task Termination Complexity: ⭐⭐⭐⭐ Difficult Safety Guarantees: No data loss, clean resource cleanup
Problem Solved
Coordinates shutdown of multiple async tasks, ensures cleanup operations complete, and prevents data loss during application termination.
Code Example
use tokio::signal;
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use std::time::Duration;
// Example 1: Signal-based shutdown
#[tokio::main]
async fn main() {
let (shutdown_tx, mut shutdown_rx) = broadcast::channel(1);
// Spawn worker tasks
for i in 0..5 {
let mut shutdown = shutdown_tx.subscribe();
tokio::spawn(async move {
loop {
tokio::select! {
_ = shutdown.recv() => {
println!("Worker {} shutting down gracefully", i);
// Cleanup operations
tokio::time::sleep(Duration::from_millis(100)).await;
println!("Worker {} finished cleanup", i);
break;
}
_ = tokio::time::sleep(Duration::from_secs(1)) => {
println!("Worker {} doing work", i);
}
}
}
});
}
// Wait for shutdown signal
signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
println!("Shutdown signal received");
// Send shutdown signal to all workers
let _ = shutdown_tx.send(());
// Wait a bit for cleanup
tokio::time::sleep(Duration::from_millis(500)).await;
println!("Application terminated");
}
// Example 2: CancellationToken pattern (recommended)
#[tokio::main]
async fn main() {
let token = CancellationToken::new();
// Spawn workers with cloned tokens
let mut handles = vec![];
for i in 0..5 {
let token = token.clone();
let handle = tokio::spawn(async move {
loop {
tokio::select! {
_ = token.cancelled() => {
println!("Worker {} received shutdown", i);
// Perform cleanup
flush_data(i).await;
break;
}
_ = tokio::time::sleep(Duration::from_secs(1)) => {
println!("Worker {} processing", i);
}
}
}
});
handles.push(handle);
}
// Wait for signal
signal::ctrl_c().await.unwrap();
println!("Initiating graceful shutdown...");
// Cancel all tasks
token.cancel();
// Wait for all workers to finish
for handle in handles {
let _ = handle.await;
}
println!("All workers shut down cleanly");
}
async fn flush_data(worker_id: usize) {
tokio::time::sleep(Duration::from_millis(100)).await;
println!("Worker {} flushed data", worker_id);
}
// Example 3: TaskTracker pattern
use tokio_util::task::TaskTracker;
#[tokio::main]
async fn main() {
let tracker = TaskTracker::new();
// Spawn tracked tasks
for i in 0..10 {
tracker.spawn(async move {
tokio::time::sleep(Duration::from_secs(i)).await;
println!("Task {} completed", i);
});
}
// Close tracker (no new tasks)
tracker.close();
// Wait for all tasks
tracker.wait().await;
println!("All tasks completed");
}
// Example 4: Complete server with graceful shutdown
use tokio::net::TcpListener;
use tokio::sync::Notify;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
let shutdown = Arc::new(Notify::new());
// Spawn signal handler
let shutdown_clone = shutdown.clone();
tokio::spawn(async move {
signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
println!("Shutdown signal received");
shutdown_clone.notify_waiters();
});
let tracker = TaskTracker::new();
loop {
tokio::select! {
// Accept new connections
result = listener.accept() => {
let (socket, addr) = result?;
println!("New connection from {}", addr);
let shutdown = shutdown.clone();
tracker.spawn(async move {
handle_connection(socket, shutdown).await;
});
}
// Shutdown signal
_ = shutdown.notified() => {
println!("No longer accepting connections");
break;
}
}
}
// Close tracker and wait for active connections
tracker.close();
println!("Waiting for {} active connections...", tracker.len());
tracker.wait().await;
println!("All connections closed. Goodbye!");
Ok(())
}
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn handle_connection(mut socket: TcpStream, shutdown: Arc<Notify>) {
let mut buf = [0; 1024];
loop {
tokio::select! {
// Read from socket
result = socket.read(&mut buf) => {
match result {
Ok(0) => break, // Connection closed
Ok(n) => {
if socket.write_all(&buf[0..n]).await.is_err() {
break;
}
}
Err(_) => break,
}
}
// Shutdown notification
_ = shutdown.notified() => {
println!("Connection closing due to shutdown");
let _ = socket.write_all(b"Server shutting down\n").await;
break;
}
}
}
}
Shutdown Pattern Comparison
| Pattern | Complexity | Use Case | Cleanup Support |
|---|---|---|---|
broadcast::channel |
Medium | Simple broadcast | ✅ Yes |
CancellationToken |
Low | Recommended default | ✅ Yes |
Notify |
Low | Single signal | ✅ Yes |
TaskTracker |
Medium | Wait for all tasks | ✅ Yes |
Best Practices:
- Always handle signals:
tokio::select! {
_ = signal::ctrl_c() => { /* shutdown */ }
_ = signal::unix::signal(SignalKind::terminate()).unwrap().recv() => { /* shutdown */ }
}
- Use timeouts for cleanup:
tokio::select! {
_ = cleanup_task() => { /* finished */ }
_ = tokio::time::sleep(Duration::from_secs(30)) => {
eprintln!("Cleanup timeout, forcing shutdown");
}
}
- Prefer CancellationToken for most use cases (lightest weight, composable)
When to Use:
- Long-running servers
- Background workers
- Data processing pipelines
- Any application with cleanup requirements
Common Pitfalls:
// ❌ BAD: Doesn't wait for cleanup
token.cancel();
// Tasks still running!
// ✅ GOOD: Wait for tasks
token.cancel();
for handle in handles {
handle.await?;
}
3.3 Channel Patterns
Pattern Name: Async Communication Primitives Complexity: ⭐⭐⭐ Moderate Safety Guarantees: Thread-safe message passing, no data races
Problem Solved
Enables safe communication between async tasks with different concurrency patterns (one-to-one, many-to-one, many-to-many).
Code Example
use tokio::sync::{mpsc, oneshot, broadcast};
use std::time::Duration;
// Example 1: MPSC (Multi-Producer, Single-Consumer)
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32); // Buffer size 32
// Spawn multiple producers
for i in 0..5 {
let tx = tx.clone();
tokio::spawn(async move {
for j in 0..3 {
tx.send(format!("Message {} from worker {}", j, i))
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
}
});
}
// Drop original sender so receiver can complete
drop(tx);
// Single consumer
while let Some(msg) = rx.recv().await {
println!("Received: {}", msg);
}
}
// Example 2: Oneshot (Request-Response Pattern)
#[tokio::main]
async fn main() {
let (tx, rx) = oneshot::channel();
// Spawn worker
tokio::spawn(async move {
let result = expensive_computation().await;
let _ = tx.send(result); // Send result back
});
// Wait for result
match rx.await {
Ok(result) => println!("Got result: {}", result),
Err(_) => println!("Worker died"),
}
}
async fn expensive_computation() -> u64 {
tokio::time::sleep(Duration::from_secs(1)).await;
42
}
// Example 3: Broadcast (Multi-Producer, Multi-Consumer)
#[tokio::main]
async fn main() {
let (tx, _rx) = broadcast::channel(16);
// Spawn multiple consumers
for i in 0..3 {
let mut rx = tx.subscribe();
tokio::spawn(async move {
while let Ok(msg) = rx.recv().await {
println!("Consumer {} received: {}", i, msg);
}
});
}
// Send messages (all consumers receive)
for i in 0..5 {
tx.send(format!("Broadcast {}", i)).unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
// Example 4: Command Pattern (mpsc + oneshot)
enum Command {
Get { key: String, resp: oneshot::Sender<Option<String>> },
Set { key: String, value: String, resp: oneshot::Sender<()> },
}
struct Cache {
rx: mpsc::Receiver<Command>,
data: std::collections::HashMap<String, String>,
}
impl Cache {
fn new() -> (CacheHandle, Self) {
let (tx, rx) = mpsc::channel(32);
let cache = Cache {
rx,
data: std::collections::HashMap::new(),
};
(CacheHandle { tx }, cache)
}
async fn run(mut self) {
while let Some(cmd) = self.rx.recv().await {
match cmd {
Command::Get { key, resp } => {
let value = self.data.get(&key).cloned();
let _ = resp.send(value);
}
Command::Set { key, value, resp } => {
self.data.insert(key, value);
let _ = resp.send(());
}
}
}
}
}
#[derive(Clone)]
struct CacheHandle {
tx: mpsc::Sender<Command>,
}
impl CacheHandle {
…(truncated)