In this lesson, you will learn how to safely share and modify state across multiple asynchronous tasks using thread-safe interior mutability.
When you move code into an async block or function, you are often handing that data off to an executor that may run it across multiple threads. If you want to share state between these tasks, you can't simply use a RefCell<T>, because RefCell is not Sync—it doesn't have the safeguards needed to prevent data races in a multi-threaded environment.
To achieve interior mutability in an async context, you need a wrapper that provides both thread safety and the ability to mutate through a shared reference. The standard tool for this is Mutex<T> (or RwLock<T>) from the std::sync module.
Why the wrapper matters
In Rust, the compiler forces us to prove that our data is safe to access concurrently. By wrapping your data in a Mutex, you satisfy the Sync requirements because the Mutex ensures that only one task can access the internal data at any given time.
Consider this example where we track a shared counter across two asynchronous tasks:
use std::sync::Mutex;
use std::sync::Arc;
async fn increment_counter(counter: Arc<Mutex<i32>>) {
let mut data = counter.lock().unwrap();
*data += 1;
}
Here, Arc allows multiple tasks to own the Mutex, and the Mutex allows those tasks to gain mutable access to the i32 inside, even though they only hold a shared reference to the Mutex itself.
Important Considerations
One subtle trap: if you hold a std::sync::MutexGuard across an await point, you might encounter issues. If the executor parks the task while holding the lock, and another task attempts to acquire that same lock on the same thread, you could end up with a deadlock or blocked executors. For long-running asynchronous tasks, you would eventually look toward asynchronous-aware synchronization primitives, but the core principle remains: interior mutability is achieved by wrapping data in a type that manages thread-safe access at runtime.
Key Takeaway: Use thread-safe interior mutability wrappers like Mutex<T> wrapped in an Arc to permit mutation of shared state across multiple asynchronous tasks.