🎓 A1Learner
🔍
0 of 45 lessons completed

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?