Rust Tokio Sync
Tokio sync is a family of locks and signals for tasks. A Tokio Mutex guards a value, and only one holder may touch it at a time.
A Mutex tasks may hold across an await
tokio::sync::Mutex locks with .await — the lock itself is async:
let counter = Arc::new(Mutex::new(0));
let mut value = counter.lock().await; // .await — the lock itself is async
*value += 1;
println!("counter = {}", *value);RwLock: many readers, or one writer
Like the std version, but async-aware. Two readers may hold it at the same time:
let lock = RwLock::new(5);
let a = lock.read().await;
let b = lock.read().await; // two readers at once is fine
println!("readers see {} and {}", *a, *b);Semaphore guards a limited number of permits
At most N tasks inside a section at once — good for rate limits and connection pools:
let gate = Semaphore::new(2); // two permits
let _p1 = gate.acquire().await.unwrap();
let _p2 = gate.acquire().await.unwrap();
println!("permits left = {}", gate.available_permits());Notify wakes waiting tasks
One task says "something changed", and the waiting tasks continue — good for signals and queues:
let notify = Arc::new(Notify::new());
let n2 = Arc::clone(¬ify);
let waiter = tokio::spawn(async move {
n2.notified().await; // waits here until told
println!("waiter woke up");
});
sleep(Duration::from_millis(10)).await;
notify.notify_one(); // "something changed"
waiter.await.unwrap();Barrier: everyone waits for everyone
A group of tasks all wait until every one has arrived — then all continue together:
let barrier = Arc::new(Barrier::new(2));
let b2 = Arc::clone(&barrier);
let first = tokio::spawn(async move {
b2.wait().await; // waits for the second arrival
println!("released");
});
barrier.wait().await; // second arrival: both go
first.await.unwrap();
println!("both tasks continued");Example
// Cargo.toml: tokio = { version = "1", features = ["full"] }
use std::sync::Arc;
use tokio::sync::{Mutex, Semaphore};
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0));
let gate = Arc::new(Semaphore::new(2)); // at most 2 tasks inside at once
let mut handles = Vec::new();
for _ in 0..5 {
let counter = Arc::clone(&counter);
let gate = Arc::clone(&gate);
handles.push(tokio::spawn(async move {
let _permit = gate.acquire().await.unwrap();
let mut value = counter.lock().await;
*value += 1;
}));
}
for h in handles { h.await.unwrap(); }
println!("counter = {}", *counter.lock().await);
}Key points
- Use the std Mutex when the lock is never held across .await and the guarded work is short — it is faster
- Holding any lock across .await can freeze the tasks waiting behind you — keep guarded work short
- Tokio's Mutex is not a faster std Mutex.
In this example, five tasks share one counter. The counter sits behind a Tokio Mutex. A semaphore with two permits lets at most two of them reach the lock at a time. Each task adds one, so the final count is 5. The lock guard is a value. When a task ends, the guard is dropped. The lock then opens for the next task.
Note
Its one job is to be safe to hold across .await — if you never await while holding the lock, the std lock wins.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
When should you prefer the std Mutex over the Tokio Mutex in async code?