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 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 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!

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 Mutexes 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.