Module blocking
Expand description
Synchronous (blocking) synchronization primitives.
The core synchronization primitives in maitake-sync, such as
Mutex, RwLock, and
WaitQueue are asynchronous. They are designed to be
used with core::task and core::future, and when it is necessary to
wait for another task to complete some work for the current task to proceed,
maitake’s synchronization primitives wait by yielding to the
asynchronous task scheduler to allow another task to proceed.
This module, on the other hand, provides synchronous (or blocking)
synchronization primitives. Rather than yielding to the runtime, these
synchronization primitives will block the current CPU core (or thread, if
running in an environment with threads) until they are woken by other cores.
These synchronization primitives are, in some cases, necessary to implement
the async synchronization primitives that form maitake-sync’s core APIs.
They are also exposed publicly in this module so that they can be used in
other projects when a blocking-based synchronization primitive is needed.
This module provides the following synchronization primitive types:
Mutex: a synchronous mutual exclusion lock.RwLock: a synchronous reader-writer lock.
§overriding mutex implementations
By default, the Mutex type uses a DefaultMutex as the underlying
blocking strategy. This type attempts to choose a suitable implementation
for the blocking mutex based on the currently available feature flags. When
the std feature is not enabled, this is typically a spinlock, which
waits for the lock to become available by spinning: repeatedly checking an
atomic value in a loop, executing spin-loop hint instructions until the
lock value changes. These spinlock implementations are represented by the
Spinlock and RwSpinlock types in the spin module.
Spinlocks are simple to implement and, thanks to the Rust standard library abstracting over atomic operations, portable. The default spinlock will work on any platform with support for atomic compare-and-swap operations.1 However, there are a number of reasons why a generic spinlock may not be desirable for all use-cases. For example:
- On single-core platforms, there is no concurrent thread of execution which can acquire and release a lock. If code running on a single-core system attempts to acquire a lock and finds that it is already locked, waiting for the lock to be released will never work, since the same thread of execution is holding the lock already. On such systems, any attempt to lock a mutex that is locked is guaranteed to be a deadlock. Therefore, code which can guarantee that it will only run on single-core CPUs may prefer to avoid the complexity of a “real” lock implementation altogether, and panic (or otherwise alert the programmer) rather than deadlocking when attempting to acquire a mutex that is already locked.
- In bare-metal code, the data protected by a mutex may be shared not only with concurrent threads of execution, but with [interrupt handlers] as well. When this is the case, it may be necessary for acquiring a lock to also disable interrupts while the lock is held (and re-enable then when the lock is released) to avoid racing with code that runs in an interrupt handler.
- While spinlocks are simple and portable, they are not the most efficient. A CPU core waiting on a spinlock draws power while in a spin loop, and is not executing any other tasks. Systems with a notion of a scheduler, whether provided by an operating system or implemented directly within a bare-metal program, may prefer to yield to the scheduler when waiting for a lock.
- Some hardware platforms provide mechanisms to optimize the performance of spinlocks, such as Hardware Lock Elision on x86 CPUs. When such features are available, using them can improve performance and efficiency.
However, all of these alternative waiting strategies knowledge of either the
underlying hardware platform, the specific details of the system, or both. A
single-core “fake” spinlock naturally requires the knowledge that the
hardware platform is single-core, and hardware lock elision similarly
requires knowledge about platform-specific features. Similarly, disabling
interrupts may require application-specific as well as hardware-specific
code, especially if only certain interrupts need to be disabled. And, of
course, yielding to a scheduler requires scheduler-specific code. Since
maitake-sync is a platform-agnostic, generic library, it does not provide
its own implementations of such behaviors. Instead, users which need a
blocking strategy other than the default spinlock may override the default
behavior using the traits provided by the mutex-traits crate.
The Mutex type in this module is generic over an additional Lock type
parameter, which represents the actual raw mutex implementation. This type
parameter defaults to the Spinlock type, but it can be overridden to an
arbitrary user-provided type. The mutex-traits crate provides two
traits representing a raw mutex, mutex_traits::ScopedRawMutex and
mutex_traits::RawMutex. These can be implemented to provide a custom
lock implementation. mutex_traits::RawMutex represents a generic
mutual-exclusion lock that can be freely locked and unlocked at any time,
while mutex_traits::ScopedRawMutex is a subset of RawMutex for which
locks can only be acquired and released for the duration of a closure. As
the functionality of RawMutex is a superset of ScopedRawMutex, all
types which implement RawMutex also implement ScopedRawMutex. In
general, it is recommended for user lock types implement the more flexible
RawMutex trait rather than ScopedRawMutex, if possible:
ScopedRawMutex exists to support more restricted lock types which
require scoped lock-and-unlock operations. Finally, the ConstInit trait
abstracts over const fn initialization of a raw mutex type, and is
required for const fn constructors with a custom raw mutex type.
When the Lock type parameter implements ScopedRawMutex, the Mutex
type provides the Mutex::with_lock and Mutex::try_with_lock methods,
which execute a closure with the mutex locked, and release the lock when the
closure returns. When the Lock type parameter also implements
RawMutex, the Mutex type provides Mutex::lock and
Mutex::try_lock methods, which return a RAII MutexGuard, similar to
the interface provided by std::sync::Mutex. The Mutex::new function
returns a Mutex using the default spinlock. To instead construct a
Mutex with a custom RawMutex implementation, use the
Mutex::new_with_raw_mutex function.
Furthermore, many async synchronization primitives provided by this crate,
such as the async Mutex, async RwLock, WaitQueue,
WaitMap, and Semaphore, internally depend on the blocking Mutex
for wait list synchronization. These types are also generic over a Lock
type parameter, and also provide new_with_raw_mutex constructors, such as
WaitQueue::new_with_raw_mutex. This allows
the blocking mutex used by these types to be overridden. The majority
maitake-sync’s async synchronization types only require the Lock type to
implement ScopedRawMutex. However, the Semaphore and async
RwLock require the more permissive RawMutex trait.
The mutex crate provides a number of types implementing RawMutex and
ScopedRawMutex, including adapters for compatibility with the
lock_api and critical-section crates.
Similarly to the RawMutex trait, the blocking RwLock type in this
module is generic over a Lock type parameter, which must implement the
RawRwLock trait. This allows the RwLock’s blocking behavior to be
overridden similarly to Mutex.
Structs§
- Default
Mutex - Default, best-effort
ScopedRawMuteximplementation. - Mutex
- A blocking mutual exclusion lock for protecting shared data.
Each mutex has a type parameter which represents
the data that it is protecting. The data can only be accessed through the
RAII guards returned from
lockandtry_lock, or within the closures passed towith_lockandtry_with_lock, which guarantees that the data is only ever accessed when the mutex is locked. - Mutex
Guard - An RAII implementation of a “scoped lock” of a mutex. When this structure is dropped (falls out of scope), the lock will be unlocked.
- RwLock
- A spinlock-based readers-writer lock.
- RwLock
Read Guard - An RAII implementation of a “scoped read lock” of a
RwLock. When this structure is dropped (falls out of scope), the lock will be unlocked. - RwLock
Write Guard - An RAII implementation of a “scoped write lock” of a
RwLock. When this structure is dropped (falls out of scope), the lock will be unlocked.
Traits§
- Const
Init - Const Init Trait
- RawMutex
- Raw mutex trait.
- RawRw
Lock - Trait abstracting over blocking
RwLockimplementations (maitake-sync’s version). - Scoped
RawMutex - Raw scoped mutex trait.