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 lock_assuming_cancel_safe
This mutex API is subtly different from most other async mutex APIs in that
its default lock
operation does not return a “smart pointer” style mutex
guard that can Deref
to access the guarded data. Instead, by default,
locking this mutex only grants you the ability to do a single action with
the guarded data, without being able to await
during the action. There is
a very good reason for this, but it’s subtle.
You’d use this default lock
API like this:
some_mutex.lock().await.perform(|data| data.squirrels += 1);
That is, you call .lock().await
to block until the mutex is yours, and
then call perform
on the result to access the guarded data. The function
provided to perform
must be a normal Rust fn
, and not an async fn
.
The mutex is released as soon as perform
runs your function. This means
there is no way to make alterations to the guarded data, await
, and then
try and make more.
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 because you will restore things before you unlock it –
but then 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! This makes the bug very
difficult to track down.
To make this class of bugs harder to write, the default lock
operation on
this mutex doesn’t allow you to access the guarded data on two sides of an
await
point.
But because this doesn’t cover every use case, and in keeping with the broader OS philosophy of letting you do potentially dangerous but powerful things, there’s an opt-in API that provides the traditional, smart-pointer style interface. To use this API, you must create your mutex in a way that asserts that you intend to keep its contents valid across any possible cancellation point. As long as you do that, this API won’t cause you any problems.
To assert this, wrap the guarded data in the CancelSafe
wrapper type,
and write the mutex type as Mutex<CancelSafe<T>>
instead of just
Mutex<T>
. Then two new operations become available:
Mutex::lock_assuming_cancel_safe
and
Mutex::try_lock_assuming_cancel_safe
. These work in the traditional way
for more complex use cases.
§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§
- A token that grants the ability to run one closure against the data guarded by a
Mutex
. - 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.