Skip to content

Q048. Why is holding a lock guard across .await dangerous?

Short Answer

At .await, the task may remain suspended for an unbounded time while still owning the guard. Other tasks needing the lock then wait, which can amplify latency, exhaust a pool, or deadlock if the awaited work directly or indirectly needs the same lock. Extract or update the necessary state, drop the guard, and only then await. Hold a guard across .await only when the resource protocol genuinely requires it and an async lock is used deliberately.

Deep Dive

The compiler preserves locals that are live across .await, including a lock guard. Lexical braces or an explicit drop can end the guard's lifetime before suspension.

With a standard mutex, contention can block an executor worker, preventing unrelated tasks from running. Some guards also make the entire future non-Send, so it cannot be spawned on a multi-thread runtime. An async mutex avoids blocking the worker while other tasks wait, but it does not remove logical contention or deadlock risk.

Often the best design is to clone a small value, perform an asynchronous operation without the lock, then reacquire the lock to commit a result. That design must account for state changes between acquisitions, possibly using a version or optimistic check.

Internal Model

An async state machine stores every local needed after a suspension point. If the guard is one of those locals, the lock remains acquired while the executor polls unrelated tasks.

Dropping the guard runs its destructor, releases the lock, and wakes or unparks a waiter according to the lock implementation.

Example

rust
use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() {
    let symbol = Arc::new(Mutex::new(String::from("BTC-USD")));

    let request_symbol = {
        let guard = symbol.lock().unwrap();
        guard.clone()
    }; // Guard is dropped here.

    tokio::task::yield_now().await;
    assert_eq!(request_symbol, "BTC-USD");
}

Common Mistakes

  • Believing .await automatically releases local lock guards.
  • Switching to an async mutex without reducing the critical section.
  • Dropping and reacquiring a lock while assuming protected state cannot change.

Follow-ups

  • How can a lock guard make a future non-Send?
  • When is holding an async mutex guard across .await justified?
  • How can optimistic concurrency protect a two-phase operation?

References

  • Async Book
  • Rust Standard Library
  • Tokio Documentation