Module lilos::mutex

source ·
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 awaiting (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.