Rust by Example: Shared State in Async
Safely share mutable state in an async environment. Learn why you should use Tokio's async Mutex instead of the standard library's Mutex when holding locks across await points to avoid deadlocks.
Code
use std::sync::Arc;
use tokio::sync::Mutex; // Note: Tokio's Mutex, not std::sync::Mutex
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// Arc<Mutex<T>> is the standard pattern for shared mutable state
let db = Arc::new(Mutex::new(vec![]));
let mut handles = vec![];
for i in 0..5 {
let db_ref = db.clone();
handles.push(tokio::spawn(async move {
// Lock the mutex. This is an async operation!
// It will yield if the lock is held by another task.
let mut data = db_ref.lock().await;
data.push(i);
println!("Task {} added data. Current len: {}", i, data.len());
// Simulate holding the lock for a while (e.g. IO operation)
// Be careful holding locks across await points!
sleep(Duration::from_millis(100)).await;
// Lock is released when 'data' goes out of scope
}));
}
for handle in handles {
handle.await.unwrap();
}
let final_data = db.lock().await;
println!("Final data: {:?}", *final_data);
}Explanation
Sharing mutable state in async code requires special care. While you can use std::sync::Mutex, it is dangerous to hold a standard mutex across an .await point. If you do, you block the entire OS thread, preventing the runtime from scheduling other tasks. If one of those other tasks is supposed to release the lock, you get a deadlock.
Instead, use tokio::sync::Mutex. Its lock() method is an async function (lock().await). If the lock is contended, it yields the task, allowing the runtime to switch to other work. This makes it safe to hold the lock across other .await calls, such as performing I/O while holding the lock.
However, async mutexes are more expensive than standard mutexes. If you only need to lock data for a very short time (no I/O, no awaits), it is often better to use std::sync::Mutex even in async code, as long as you strictly ensure the lock is dropped before any .await.
Code Breakdown
use tokio::sync::Mutex. We explicitly import Tokio's Mutex. It is designed to work with the async runtime's scheduler and provides an async lock() method.db_ref.lock().await. We await the lock. This is the key difference. A standard mutex would block the thread here, potentially starving other tasks.sleep(...).await. We are holding the lock while sleeping. Because we use Tokio's mutex, this is safe. The thread is freed to run other tasks, but the lock remains held by this task.Arc::new(...). Just like in sync Rust, we use Arc to share ownership of the Mutex across multiple tasks.
