Module maitake_sync::blocking

source ·
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:

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.


  1. Including those where “atomics” are implemented by the portable-atomic crate, as described here

Structs

  • Default, best-effort ScopedRawMutex implementation.
  • 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 lock and try_lock, or within the closures passed to with_lock and try_with_lock, which guarantees that the data is only ever accessed when the mutex is locked.
  • An RAII implementation of a “scoped lock” of a mutex. When this structure is dropped (falls out of scope), the lock will be unlocked.
  • A spinlock-based readers-writer lock.
  • 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.
  • 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