What is an "async" mutex as opposed to a "normal" mutex? I believe this is the difference between tokio's Mutex
and the normal std lib Mutex
. But I don't get, conceptually, how a mutex can be "async". Isn't the whole point that only one thing can use it at a time?
1 Answers
Here's a simple comparison of their usage:
let mtx = std::sync::Mutex::new(0);
let _guard = mtx.lock().unwrap();
let mtx = tokio::sync::Mutex::new(0);
let _guard = mtx.lock().await;
Both ensure mutual exclusivity. The only difference between an asynchronous mutex and a synchronous mutex is dictated by their behavior when trying to acquire a lock. If a synchronous mutex tries to acquire the lock while it is already locked, it will block execution on the thread. If an asynchronous mutex tries to acquire the lock while it is already locked, it will yield execution to the executor.
If your code is synchronous, there's no reason to use an asynchronous mutex. As shown above, locking an asynchronous mutex is Future
-based and is designed to be using in async
/await
contexts.
If your code is asynchronous, you may still want to use a synchronous mutex since there is less overhead. However, you should be mindful that blocking in an async
/await
context is to be avoided at all costs. Therefore, you should only use a synchronous mutex if acquiring the lock is not expected to block. Some cases to keep in mind:
- If you need to hold the lock over an
.await
call, use an asynchronous mutex. The compiler will usually reject this anyway when using thread-safe futures since most synchronous mutex locks can't be sent to another thread. - If your lock is contentious (i.e. if you expect the mutex to already be locked when you want it), you should use an asynchronous mutex. This can happen when synchronizing multiple tasks into a pool or bounded queue.
- If you have complicated and/or computationally-heavy updates, those should probably be moved to a blocking pool anyway where you'd use a synchronous mutex.
The above cases are all three sides of the same coin: if you expect to block, use an asynchronous mutex. If you don't know whether your mutex usage will block or not, err on the side of caution and use an asynchronous mutex. Using an asynchronous mutex where a synchronous one would suffice only leaves a small amount of performance on the table, but using a synchronous mutex where you should've used an asynchronous one could be catastrophic.
Most situations I run into with mutexes are when synchronizing simple data structures, where the update methods are well-encapsulated to acquire the lock, update the data, and release the lock. Did you know a simple println!
requires locking a mutex? Those uses of mutexes can be synchronous and used even in an asynchronous context. Even if the lock does block, it often is no more impactful than a process context switch which happens all the time anyway.
Note: Tokio's Mutex
does have a .blocking_lock()
method which is helpful if both locking behaviors are needed. So the mutex can be both synchronous and asynchronous!
See also:
- Why do I get a deadlock when using Tokio with a std::sync::Mutex?
std::sync::Mutex
vsfutures:lock:Mutex
vsfutures_lock::Mutex
for async on the Rust forum- Which kind of mutex should you use? in the Tokio documentation
- On using
std::sync::Mutex
in the Tokio tutorial on shared state

- 42,554
- 6
- 57
- 106
-
That's a fine answer, but what happens when a task with an acquired async lock yields? – rsalmei Apr 16 '23 at 20:36
-
1@rsalmei Nothing special happens; the lock is still held by the task even while suspended. It can only be unlocked if the task resumes to release it or if the task is dropped entirely. – kmdreko Apr 16 '23 at 20:55
-
Thanks, @kmdreko, but how exactly does that happen? Why is an async lock needed to keep locks across a .await point? Well, I know what physically acquires a lock is an OS thread, and in an async engine, every time a task yields from an engine thread (an actual OS thread) another one that happens to be ready assumes its place. But of course, this OS thread should not have the lock acquired anymore, so it has to relinquish it somehow, doesn't it? – rsalmei Apr 17 '23 at 22:21
-
@rsalmei The lock is not released just because the task was suspended, that would defeat the purpose. You *can* hold a synchronous lock across an `.await`, but many async frameworks require thread-safe tasks by default and the standard `Mutex`'s lock guard is *not* thread-safe so you'd often run into issues there. You could use the [`Mutex`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html) from the parking-lot crate if you wanted to since it's guard is thread-safe. – kmdreko Apr 18 '23 at 03:33
-
1The reason you should consider an asynchronous lock when it is held over `.await`s is not exactly due to the thread-safety issue but rather because during a `.await` the task is suspended (and therefore the lock is held) for an unpredictable amount of time (often due to I/O). Thus when acquiring the lock elsewhere in an asynchronous context, you know it may be held for potentially a long time, so you want to use an asynchronous mutex to avoid blocking the executor. You can even get yourself into deadlocks if you don't (depending on the executor's work-stealing behavior). – kmdreko Apr 18 '23 at 03:34