What is the difference between std::sync::Mutex vs tokio::sync::Mutex?
Asked Answered
T

1

23

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?

Tome answered 24/9, 2022 at 20:54 Comment(0)
A
37

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:

Ammamaria answered 24/9, 2022 at 21:58 Comment(5)
That's a fine answer, but what happens when a task with an acquired async lock yields?Glynnis
@Glynnis 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.Ammamaria
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?Glynnis
@Glynnis 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 from the parking-lot crate if you wanted to since it's guard is thread-safe.Ammamaria
The reason you should consider an asynchronous lock when it is held over .awaits 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).Ammamaria

© 2022 - 2025 — McMap. All rights reserved.