| //===--- Implementation of the RawRwLock class -----------------*- C++ -*-===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| #ifndef LLVM_LIBC_SRC___SUPPORT_THREADS_RAW_RWLOCK_H |
| #define LLVM_LIBC_SRC___SUPPORT_THREADS_RAW_RWLOCK_H |
| |
| #include "hdr/errno_macros.h" |
| #include "src/__support/CPP/atomic.h" |
| #include "src/__support/CPP/limits.h" |
| #include "src/__support/CPP/optional.h" |
| #include "src/__support/common.h" |
| #include "src/__support/libc_assert.h" |
| #include "src/__support/macros/attributes.h" |
| #include "src/__support/macros/config.h" |
| #include "src/__support/macros/optimization.h" |
| #include "src/__support/threads/raw_mutex.h" |
| #include "src/__support/threads/sleep.h" |
| |
| #ifndef LIBC_COPT_RWLOCK_DEFAULT_SPIN_COUNT |
| #define LIBC_COPT_RWLOCK_DEFAULT_SPIN_COUNT 100 |
| #endif |
| |
| #ifdef LIBC_COPT_TIMEOUT_ENSURE_MONOTONICITY |
| #include "src/__support/time/monotonicity.h" |
| #endif |
| |
| namespace LIBC_NAMESPACE_DECL { |
| // A namespace to rwlock specific utilities. |
| namespace rwlock { |
| // The role of the thread in the RwLock. |
| enum class Role { Reader = 0, Writer = 1 }; |
| |
| enum class LockResult : int { |
| Success = 0, |
| TimedOut = ETIMEDOUT, |
| Overflow = EAGAIN, |
| Busy = EBUSY, |
| Deadlock = EDEADLOCK, |
| PermissionDenied = EPERM, |
| }; |
| |
| // A waiting queue to keep track of the pending readers and writers. |
| class WaitingQueue final : private RawMutex { |
| /* FutexWordType raw_mutex; (from base class) */ |
| |
| // Pending reader count (protected by the mutex) |
| FutexWordType pending_readers; |
| // Pending writer count (protected by the mutex) |
| FutexWordType pending_writers; |
| // Reader serialization (increases on each reader-waking operation) |
| Futex reader_serialization; |
| // Writer serialization (increases on each writer-waking operation) |
| Futex writer_serialization; |
| |
| public: |
| // RAII guard to lock and unlock the waiting queue. |
| class Guard { |
| WaitingQueue &queue; |
| bool is_pshared; |
| |
| LIBC_INLINE Guard(WaitingQueue &queue, bool is_pshared) |
| : queue(queue), is_pshared(is_pshared) { |
| queue.lock(cpp::nullopt, is_pshared); |
| } |
| |
| public: |
| LIBC_INLINE ~Guard() { queue.unlock(is_pshared); } |
| template <Role role> LIBC_INLINE FutexWordType &pending_count() { |
| if constexpr (role == Role::Reader) |
| return queue.pending_readers; |
| else |
| return queue.pending_writers; |
| } |
| template <Role role> LIBC_INLINE Futex &serialization() { |
| if constexpr (role == Role::Reader) |
| return queue.reader_serialization; |
| else |
| return queue.writer_serialization; |
| } |
| friend WaitingQueue; |
| }; |
| |
| public: |
| LIBC_INLINE constexpr WaitingQueue() |
| : RawMutex(), pending_readers(0), pending_writers(0), |
| reader_serialization(0), writer_serialization(0) {} |
| |
| LIBC_INLINE Guard acquire(bool is_pshared) { |
| return Guard(*this, is_pshared); |
| } |
| |
| template <Role role> |
| LIBC_INLINE ErrorOr<int> wait(FutexWordType expected, |
| cpp::optional<Futex::Timeout> timeout, |
| bool is_pshared) { |
| if constexpr (role == Role::Reader) |
| return reader_serialization.wait(expected, timeout, is_pshared); |
| else |
| return writer_serialization.wait(expected, timeout, is_pshared); |
| } |
| |
| template <Role role> LIBC_INLINE ErrorOr<int> notify(bool is_pshared) { |
| if constexpr (role == Role::Reader) |
| return reader_serialization.notify_all(is_pshared); |
| else |
| return writer_serialization.notify_one(is_pshared); |
| } |
| }; |
| |
| // The RwState of the RwLock is stored in an integer word, consisting of the |
| // following components: |
| // ----------------------------------------------- |
| // | Range | Description | |
| // =============================================== |
| // | 0 | Pending Reader Bit | |
| // ----------------------------------------------- |
| // | 1 | Pending Writer Bit | |
| // ----------------------------------------------- |
| // | [2, MSB) | Active Reader Count | |
| // ----------------------------------------------- |
| // | MSB | Active Writer Bit | |
| // ----------------------------------------------- |
| class RwState { |
| // Shift amounts to access the components of the state. |
| LIBC_INLINE_VAR static constexpr int PENDING_READER_SHIFT = 0; |
| LIBC_INLINE_VAR static constexpr int PENDING_WRITER_SHIFT = 1; |
| LIBC_INLINE_VAR static constexpr int ACTIVE_READER_SHIFT = 2; |
| LIBC_INLINE_VAR static constexpr int ACTIVE_WRITER_SHIFT = |
| cpp::numeric_limits<int>::digits; |
| |
| // Bitmasks to access the components of the state. |
| LIBC_INLINE_VAR static constexpr int PENDING_READER_BIT = |
| 1 << PENDING_READER_SHIFT; |
| LIBC_INLINE_VAR static constexpr int PENDING_WRITER_BIT = |
| 1 << PENDING_WRITER_SHIFT; |
| LIBC_INLINE_VAR static constexpr int ACTIVE_READER_COUNT_UNIT = |
| 1 << ACTIVE_READER_SHIFT; |
| LIBC_INLINE_VAR static constexpr int ACTIVE_WRITER_BIT = |
| 1 << ACTIVE_WRITER_SHIFT; |
| LIBC_INLINE_VAR static constexpr int PENDING_MASK = |
| PENDING_READER_BIT | PENDING_WRITER_BIT; |
| |
| private: |
| // We use the signed integer as the state type. It is easier |
| // to reason about the state transitions using signness. |
| int state; |
| |
| public: |
| // Construction and conversion functions. |
| LIBC_INLINE constexpr RwState(int state = 0) : state(state) {} |
| LIBC_INLINE constexpr operator int() const { return state; } |
| |
| // Utilities to check the state of the RwLock. |
| LIBC_INLINE constexpr bool has_active_writer() const { return state < 0; } |
| LIBC_INLINE constexpr bool has_active_reader() const { |
| return state >= ACTIVE_READER_COUNT_UNIT; |
| } |
| LIBC_INLINE constexpr bool has_active_owner() const { |
| return has_active_reader() || has_active_writer(); |
| } |
| LIBC_INLINE constexpr bool has_last_reader() const { |
| return (state >> ACTIVE_READER_SHIFT) == 1; |
| } |
| LIBC_INLINE constexpr bool has_pending_writer() const { |
| return state & PENDING_WRITER_BIT; |
| } |
| LIBC_INLINE constexpr bool has_pending() const { |
| return state & PENDING_MASK; |
| } |
| |
| LIBC_INLINE constexpr RwState set_writer_bit() const { |
| return RwState(state | ACTIVE_WRITER_BIT); |
| } |
| |
| // The preference parameter changes the behavior of the lock acquisition |
| // if there are both readers and writers waiting for the lock. If writers |
| // are preferred, reader acquisition will be blocked until all pending |
| // writers are served. |
| template <Role role> LIBC_INLINE bool can_acquire(Role preference) const { |
| if constexpr (role == Role::Reader) { |
| switch (preference) { |
| case Role::Reader: |
| return !has_active_writer(); |
| case Role::Writer: |
| return !has_active_writer() && !has_pending_writer(); |
| } |
| __builtin_unreachable(); |
| } else |
| return !has_active_owner(); |
| } |
| |
| // This function check if it is possible to grow the reader count without |
| // overflowing the state. |
| LIBC_INLINE cpp::optional<RwState> try_increase_reader_count() const { |
| LIBC_ASSERT(!has_active_writer() && |
| "try_increase_reader_count shall only be called when there " |
| "is no active writer."); |
| RwState res; |
| if (LIBC_UNLIKELY(__builtin_sadd_overflow(state, ACTIVE_READER_COUNT_UNIT, |
| &res.state))) |
| return cpp::nullopt; |
| return res; |
| } |
| |
| // Utilities to do atomic operations on the state. |
| LIBC_INLINE static RwState fetch_sub_reader_count(cpp::Atomic<int> &target, |
| cpp::MemoryOrder order) { |
| return RwState(target.fetch_sub(ACTIVE_READER_COUNT_UNIT, order)); |
| } |
| |
| LIBC_INLINE static RwState load(cpp::Atomic<int> &target, |
| cpp::MemoryOrder order) { |
| return RwState(target.load(order)); |
| } |
| |
| template <Role role> |
| LIBC_INLINE static RwState fetch_set_pending_bit(cpp::Atomic<int> &target, |
| cpp::MemoryOrder order) { |
| if constexpr (role == Role::Reader) |
| return RwState(target.fetch_or(PENDING_READER_BIT, order)); |
| else |
| return RwState(target.fetch_or(PENDING_WRITER_BIT, order)); |
| } |
| template <Role role> |
| LIBC_INLINE static RwState fetch_clear_pending_bit(cpp::Atomic<int> &target, |
| cpp::MemoryOrder order) { |
| if constexpr (role == Role::Reader) |
| return RwState(target.fetch_and(~PENDING_READER_BIT, order)); |
| else |
| return RwState(target.fetch_and(~PENDING_WRITER_BIT, order)); |
| } |
| |
| LIBC_INLINE static RwState fetch_clear_active_writer(cpp::Atomic<int> &target, |
| cpp::MemoryOrder order) { |
| return RwState(target.fetch_and(~ACTIVE_WRITER_BIT, order)); |
| } |
| |
| LIBC_INLINE bool compare_exchange_weak_with(cpp::Atomic<int> &target, |
| RwState desired, |
| cpp::MemoryOrder success_order, |
| cpp::MemoryOrder failure_order) { |
| return target.compare_exchange_weak(state, desired, success_order, |
| failure_order); |
| } |
| |
| // Utilities to spin and reload the state. |
| private: |
| template <class F> |
| LIBC_INLINE static RwState spin_reload_until(cpp::Atomic<int> &target, |
| F &&func, unsigned spin_count) { |
| for (;;) { |
| auto state = RwState::load(target, cpp::MemoryOrder::RELAXED); |
| if (func(state) || spin_count == 0) |
| return state; |
| sleep_briefly(); |
| spin_count--; |
| } |
| } |
| |
| public: |
| template <Role role> |
| LIBC_INLINE static RwState spin_reload(cpp::Atomic<int> &target, |
| Role preference, unsigned spin_count) { |
| if constexpr (role == Role::Reader) { |
| // Return the reader state if either the lock is available or there is |
| // any ongoing contention. |
| return spin_reload_until( |
| target, |
| [=](RwState state) { |
| return state.can_acquire<Role::Reader>(preference) || |
| state.has_pending(); |
| }, |
| spin_count); |
| } else { |
| // Return the writer state if either the lock is available or there is |
| // any contention *between writers*. Since writers can be way less than |
| // readers, we allow them to spin more to improve the fairness. |
| return spin_reload_until( |
| target, |
| [=](RwState state) { |
| return state.can_acquire<Role::Writer>(preference) || |
| state.has_pending_writer(); |
| }, |
| spin_count); |
| } |
| } |
| |
| friend class RwLockTester; |
| }; |
| } // namespace rwlock |
| |
| class RawRwLock { |
| using RwState = rwlock::RwState; |
| using Role = rwlock::Role; |
| using WaitingQueue = rwlock::WaitingQueue; |
| |
| public: |
| // Return types for the lock functions. |
| // All the locking routines returning this type are marked as [[nodiscard]] |
| // because it is a common error to assume the lock success without checking |
| // the return value, which can lead to undefined behaviors or other subtle |
| // bugs that are hard to reason about. |
| using LockResult = rwlock::LockResult; |
| |
| private: |
| // Whether the RwLock is shared between processes. |
| LIBC_PREFERED_TYPE(bool) |
| unsigned is_pshared : 1; |
| // Reader/Writer preference. |
| LIBC_PREFERED_TYPE(Role) |
| unsigned preference : 1; |
| // RwState to keep track of the RwLock. |
| cpp::Atomic<int> state; |
| // Waiting queue to keep track of the readers and writers. |
| WaitingQueue queue; |
| |
| private: |
| // Load the bitfield preference. |
| LIBC_INLINE Role get_preference() const { |
| return static_cast<Role>(preference); |
| } |
| |
| template <Role role> LIBC_INLINE LockResult try_lock(RwState &old) { |
| if constexpr (role == Role::Reader) { |
| while (LIBC_LIKELY(old.can_acquire<Role::Reader>(get_preference()))) { |
| cpp::optional<RwState> next = old.try_increase_reader_count(); |
| if (!next) |
| return LockResult::Overflow; |
| if (LIBC_LIKELY(old.compare_exchange_weak_with( |
| state, *next, cpp::MemoryOrder::ACQUIRE, |
| cpp::MemoryOrder::RELAXED))) |
| return LockResult::Success; |
| // Notice that old is updated by the compare_exchange_weak_with |
| // function. |
| } |
| return LockResult::Busy; |
| } else { |
| // This while loop should terminate quickly |
| while (LIBC_LIKELY(old.can_acquire<Role::Writer>(get_preference()))) { |
| if (LIBC_LIKELY(old.compare_exchange_weak_with( |
| state, old.set_writer_bit(), cpp::MemoryOrder::ACQUIRE, |
| cpp::MemoryOrder::RELAXED))) |
| return LockResult::Success; |
| // Notice that old is updated by the compare_exchange_weak_with |
| // function. |
| } |
| return LockResult::Busy; |
| } |
| } |
| |
| template <Role role> |
| LIBC_INLINE LockResult |
| lock_slow(cpp::optional<Futex::Timeout> timeout = cpp::nullopt, |
| unsigned spin_count = LIBC_COPT_RWLOCK_DEFAULT_SPIN_COUNT) { |
| #ifdef LIBC_COPT_TIMEOUT_ENSURE_MONOTONICITY |
| // Phase 2: convert the timeout if necessary. |
| if (timeout) |
| ensure_monotonicity(*timeout); |
| #endif |
| |
| // Phase 3: spin to get the initial state. We ignore the timing due to |
| // spin since it should end quickly. |
| RwState old = |
| RwState::spin_reload<role>(state, get_preference(), spin_count); |
| |
| // Enter the main acquisition loop. |
| for (;;) { |
| // Phase 4: if the lock can be acquired, try to acquire it. |
| LockResult result = try_lock<role>(old); |
| if (result != LockResult::Busy) |
| return result; |
| |
| // Phase 5: register ourselves as a reader. |
| FutexWordType serial_number; |
| { |
| // The queue need to be protected by a mutex since the operations in |
| // this block must be executed as a whole transaction. It is possible |
| // that this lock will make the timeout imprecise, but this is the |
| // best we can do. The transaction is small and everyone should make |
| // progress rather quickly. |
| WaitingQueue::Guard guard = queue.acquire(is_pshared); |
| guard.template pending_count<role>()++; |
| |
| // Use atomic operation to guarantee the total order of the operations |
| // on the state. The pending flag update should be visible to any |
| // succeeding unlock events. Or, if a unlock does happen before we |
| // sleep on the futex, we can avoid such waiting. |
| old = RwState::fetch_set_pending_bit<role>(state, |
| cpp::MemoryOrder::RELAXED); |
| // relaxed atomic since it is already protected by the mutex. |
| serial_number = |
| guard.serialization<role>().load(cpp::MemoryOrder::RELAXED); |
| } |
| |
| // Phase 6: do futex wait until the lock is available or timeout is |
| // reached. |
| bool timeout_flag = false; |
| if (!old.can_acquire<role>(get_preference())) { |
| ErrorOr<int> wait_result = |
| queue.wait<role>(serial_number, timeout, is_pshared); |
| timeout_flag = |
| (!wait_result.has_value() && wait_result.error() == ETIMEDOUT); |
| } |
| |
| // Phase 7: unregister ourselves as a pending reader/writer. |
| bool writer_serial_changed = false; |
| { |
| // Similarly, the unregister operation should also be an atomic |
| // transaction. |
| WaitingQueue::Guard guard = queue.acquire(is_pshared); |
| guard.pending_count<role>()--; |
| // Clear the flag if we are the last pending thread of this role. The |
| // flag must be cleared; otherwise operations like trylock may fail even |
| // though there are no competitors. |
| if (guard.pending_count<role>() == 0) |
| RwState::fetch_clear_pending_bit<role>(state, |
| cpp::MemoryOrder::RELAXED); |
| if constexpr (role == Role::Writer) { |
| FutexWordType new_serial = |
| guard.serialization<role>().load(cpp::MemoryOrder::RELAXED); |
| writer_serial_changed = new_serial != serial_number; |
| } |
| } |
| |
| // Phase 8: exit the loop if timeout is reached. |
| if (timeout_flag) { |
| // A timed out (but not unregistered) thread can still be the target of |
| // a wakeup signal. If this happens, we must wake up the next thread to |
| // avoid a deadlock. This is only a problem for writers because readers |
| // are woken up all at once and we prefer waking up writers first. |
| if (writer_serial_changed) |
| notify_pending_threads(); |
| return LockResult::TimedOut; |
| } |
| |
| // Phase 9: reload the state and retry the acquisition. |
| old = RwState::spin_reload<role>(state, get_preference(), spin_count); |
| } |
| } |
| |
| // Compiler (clang 19.0) somehow decides that this function may be inlined, |
| // which leads to a larger unlock function that is infeasible to be inlined. |
| // Since notifcation routine is colder we mark it as noinline explicitly. |
| [[gnu::noinline]] |
| LIBC_INLINE void notify_pending_threads() { |
| // We wake up writers first. This ordering is relevant for the correctness |
| // of timeout handling in lock_slow. Because writers are preferred, we do |
| // not need to handle reader timeouts specially: |
| // 1. If there are pending writers, they are woken up first, so a reader |
| // cannot steal their wake-up signal. |
| // 2. If there are only pending readers, they are all woken up together |
| // (via notify_all), so a timed-out reader cannot steal other readers' |
| // wake-up signals. |
| enum class WakeTarget { Readers, Writers, None }; |
| WakeTarget status; |
| |
| { |
| WaitingQueue::Guard guard = queue.acquire(is_pshared); |
| if (guard.pending_count<Role::Writer>() != 0) { |
| guard.serialization<Role::Writer>().fetch_add( |
| 1, cpp::MemoryOrder::RELEASE); |
| status = WakeTarget::Writers; |
| } else if (guard.pending_count<Role::Reader>() != 0) { |
| guard.serialization<Role::Reader>().fetch_add( |
| 1, cpp::MemoryOrder::RELEASE); |
| status = WakeTarget::Readers; |
| } else { |
| status = WakeTarget::None; |
| } |
| } |
| |
| if (status == WakeTarget::Readers) |
| queue.notify<Role::Reader>(is_pshared); |
| else if (status == WakeTarget::Writers) |
| queue.notify<Role::Writer>(is_pshared); |
| } |
| |
| public: |
| LIBC_INLINE bool has_active_writer() { |
| return RwState::load(state, cpp::MemoryOrder::RELAXED).has_active_writer(); |
| } |
| |
| LIBC_INLINE constexpr RawRwLock(Role preference = Role::Reader, |
| bool is_pshared = false) |
| : is_pshared(is_pshared), |
| preference(static_cast<unsigned>(preference) & 1u), state(0), queue() {} |
| |
| [[nodiscard]] |
| LIBC_INLINE LockResult try_read_lock() { |
| RwState old = RwState::load(state, cpp::MemoryOrder::RELAXED); |
| return try_lock<Role::Reader>(old); |
| } |
| |
| [[nodiscard]] |
| LIBC_INLINE LockResult try_write_lock() { |
| RwState old = RwState::load(state, cpp::MemoryOrder::RELAXED); |
| return try_lock<Role::Writer>(old); |
| } |
| |
| [[nodiscard]] |
| LIBC_INLINE LockResult |
| read_lock(cpp::optional<Futex::Timeout> timeout = cpp::nullopt, |
| unsigned spin_count = LIBC_COPT_RWLOCK_DEFAULT_SPIN_COUNT) { |
| LockResult result = try_read_lock(); |
| if (LIBC_LIKELY(result != LockResult::Busy)) |
| return result; |
| return lock_slow<Role::Reader>(timeout, spin_count); |
| } |
| |
| [[nodiscard]] |
| LIBC_INLINE LockResult |
| write_lock(cpp::optional<Futex::Timeout> timeout = cpp::nullopt, |
| unsigned spin_count = LIBC_COPT_RWLOCK_DEFAULT_SPIN_COUNT) { |
| LockResult result = try_write_lock(); |
| if (LIBC_LIKELY(result != LockResult::Busy)) |
| return result; |
| return lock_slow<Role::Writer>(timeout, spin_count); |
| } |
| |
| [[nodiscard]] |
| LIBC_INLINE LockResult unlock() { |
| RwState old = RwState::load(state, cpp::MemoryOrder::RELAXED); |
| if (old.has_active_writer()) { |
| // The lock is held by a writer. |
| // clear the writer bit. |
| old = |
| RwState::fetch_clear_active_writer(state, cpp::MemoryOrder::RELEASE); |
| // If there is no pending readers or writers, we are done. |
| if (!old.has_pending()) |
| return LockResult::Success; |
| } else if (old.has_active_reader()) { |
| // The lock is held by readers. |
| // Decrease the reader count. |
| old = RwState::fetch_sub_reader_count(state, cpp::MemoryOrder::RELEASE); |
| // If there is no pending readers or writers, we are done. |
| if (!old.has_last_reader() || !old.has_pending()) |
| return LockResult::Success; |
| } else { |
| return LockResult::PermissionDenied; |
| } |
| |
| notify_pending_threads(); |
| return LockResult::Success; |
| } |
| |
| [[nodiscard]] |
| LIBC_INLINE LockResult check_for_destroy() { |
| // We do not allocate any special resources for the RwLock, so this function |
| // will only check if the lock is currently held by any thread. |
| RwState old = RwState::load(state, cpp::MemoryOrder::RELAXED); |
| if (old.has_active_owner()) |
| return LockResult::Busy; |
| return LockResult::Success; |
| } |
| }; |
| |
| } // namespace LIBC_NAMESPACE_DECL |
| |
| #endif // LLVM_LIBC_SRC___SUPPORT_THREADS_RAW_RWLOCK_H |