Expand description
Fair mutex that must be pinned.
This implements a mutex (a kind of lock) guarding a value of type T
.
Creating a Mutex
by hand is somewhat involved (see Mutex::new
for
details), so there’s a convenience macro,
create_mutex!
.
If you don’t want to store a value inside the mutex, use a Mutex<()>
.
lock
vs perform
This mutex API is subtly different from most other async mutex APIs in that
it does not expose an RAII-style mutex-guard-based lock
operation by
default. By default, the operation you get is perform
. There is a very
good reason for this.
perform
takes a function of your choice as an argument, and when it
successfully locks the mutex, it will apply that function and unlock. The
function must be a normal Rust fn
, and not an async fn
. This means
that there is no opportunity to await
with the mutex locked.
This helps to avoid a common implementation mistake in software using
mutexes: assuming that you can temporarily violate invariants on the data
guarded by the mutex, but await
ing (and thus accepting possible
cancellation) while those invariants are still being violated. This can
cause the next observer of the guarded data to find the data in an invalid
state, often leading to panics – but not panics in the code that had the
bug!
Because that’s annoying, you have to opt-in to API that lets you make the mistake. In many cases it’s perfectly safe to opt-in to that, because the mistake can’t actually happen – if the type contained within the Mutex guards its own invariants and can’t ever be in an invalid state, then you’re not at risk.
However, some amount of application context is needed to judge whether a
type can actually protect all its invariants. For instance, in one
application’s Mutex<Option<T>>
, the application may never expect to leave
None
in there when the Mutex
is unlocked – but another application may
be just fine with that. For this reason, the ability to leave a mutex locked
across await points is not an attribute of the contained type T
. It’s
instead implemented using a wrapper type, CancelSafe
.
To declare a Mutex
that includes the lock
and try_lock
operation,
write it as Mutex<CancelSafe<T>>
instead of just Mutex<T>
.
The perform
-based API also prevents you from blocking to wait on another
Mutex
while you have one locked, which on the one hand makes certain
classes of deadlock impossible, but on the other hand can be pretty
limiting. If you need to lock a series of Mutex
es with blocking, make sure
the contents of all but the innermost are CancelSafe
!
Implementation details
This implementation uses a wait-list to track all processes that are waiting to unlock the mutex. (An OS task may contain many processes.) This makes unlocking more expensive, but means that the unlock operation is fair, preventing starvation of contending tasks.
However, in exchange for this property, mutexes must be pinned, which makes
using them slightly more awkward. See the macros
create_mutex!
and
create_static_mutex!
for convenient shorthand (or as examples of how to
do it yourself).
Structs
- Newtype to wrap the contents of a
Mutex
when you know, in the context of the current application, that it is okay to unlock this mutex at any cancellation point. - Holds a
T
that can be accessed from multiple concurrent futures/tasks, but only one at a time. - Smart pointer representing successful locking of a mutex.