๐ŸŽ“ A1Learner
๐Ÿ”

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(&notify);
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?