// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. #include #include "bvar/collector.h" #include "butil/memory/scope_guard.h" #include "bthread/rwlock.h" #include "bthread/mutex.h" #include "bthread/butex.h" namespace bthread { // Defined in bthread/mutex.cpp; reused here so that bthread_rwlock_t // participates in the global ContentionProfiler just like bthread_mutex_t // and bthread_sem_t. class ContentionProfiler; extern ContentionProfiler* g_cp; extern bvar::CollectorSpeedLimit g_cp_sl; extern void submit_contention(const bthread_contention_site_t& csite, int64_t now_ns); // Lazily arm sampling on first contention. Caller must declare // `size_t sampling_range' and `int64_t start_ns' in scope: // start_ns == 0 -> not yet decided // start_ns == -1 -> decided NOT to sample (profiler off / not selected) // start_ns > 0 -> sampling armed; value is the wall-clock start time #define BTHREAD_RWLOCK_MAYBE_START_SAMPLING \ do { \ if (start_ns == 0) { \ if (BAIDU_UNLIKELY(g_cp != nullptr)) { \ sampling_range = bvar::is_collectable(&g_cp_sl); \ start_ns = bvar::is_sampling_range_valid(sampling_range) ? \ butil::cpuwide_time_ns() : -1; \ } else { \ start_ns = -1; \ } \ } \ } while (0) // Submit one contention sample if sampling was armed for this attempt. // `start_ns > 0' is the convention used everywhere in this file to indicate // that BTHREAD_RWLOCK_MAYBE_START_SAMPLING actually decided to sample. // No-op otherwise. Force-inlined so the uncontended fast path stays cheap. static BUTIL_FORCE_INLINE void submit_contention_if_sampled( int64_t start_ns, size_t sampling_range) { if (BAIDU_UNLIKELY(start_ns > 0)) { const int64_t end_ns = butil::cpuwide_time_ns(); const bthread_contention_site_t csite{end_ns - start_ns, sampling_range}; submit_contention(csite, end_ns); } } // bthread RWLock // writer-priority implementation overview // Three synchronization fields are used: // // * `lock_word' (32-bit butex): // bit 31 : 1 if the write lock is held, 0 otherwise. // bit 0~30: number of readers currently holding the read lock. // Mutually exclusive: when bit 31 is set, the lower 31 bits are 0. // // * `writer_wait_count' (32-bit butex): // Number of writers that have entered wrlock() but not yet finished // (i.e. currently waiting for the mutex / waiting for lock_word==0 / // holding the write lock). Each writer accounts for itself: it is // incremented at the very beginning of wrlock() and decremented at // the very end of unwrlock()/cleanup(). // Readers consult this field to implement writer-priority: if any // writer is "in flight", new readers yield by waiting on it. // // * `writer_queue_mutex' (bthread_mutex_t): // Serializes writers so that at most one writer races for `lock_word' // at any time. Other writers queue up on this mutex. // // Wakeup channels: // * Readers waiting on writers -> wait on writer_wait_count, woken by unwrlock/cleanup // * Writers waiting on readers -> wait on lock_word, woken by unrdlock // * Writers waiting on writers -> wait on writer_queue_mutex static int rwlock_rdlock(bthread_rwlock_t* rwlock, bool try_lock, const struct timespec* abstime) { auto lock_word = (butil::atomic*)rwlock->lock_word; auto writer_wait_count = (butil::atomic*)rwlock->writer_wait_count; // Sampling state for the contention profiler (lazily armed on first // contention so that the uncontended fast path stays cheap): // start_ns == 0 -> not yet decided // start_ns == -1 -> decided NOT to sample // start_ns > 0 -> sampling armed; submit on exit // Each reader samples independently and submits once on its own way out; // we deliberately do NOT use rwlock->writer_csite here because that field // is exclusively owned by the writer. size_t sampling_range = bvar::INVALID_SAMPLING_RANGE; int64_t start_ns = 0; int rc = 0; while (true) { // Writer-priority: if any writer is in flight, yield to it. // `relaxed' is sufficient here because: // - There is no data published via writer_wait_count; // data visibility is established via the acquire-CAS on // `lock_word' below paired with the release-CAS in unwrlock(). // - butex_wait() will re-check the expected value before sleeping, // so we cannot lose a wakeup even if `w' is slightly stale. unsigned w = writer_wait_count->load(butil::memory_order_relaxed); if (w > 0) { if (try_lock) { // Don't sample tryrdlock failures: they are by design a // non-blocking probe, not a contention event. return EBUSY; } // We are about to block on writer_wait_count; arm sampling // before parking so the wait time is included in the report. BTHREAD_RWLOCK_MAYBE_START_SAMPLING; if (butex_wait(writer_wait_count, w, abstime) < 0 && errno != EWOULDBLOCK && errno != EINTR) { rc = errno; break; } continue; } // No writer in flight: try to add ourselves to the reader count. // 2^31 - 1 readers should be enough for any realistic workload. unsigned l = lock_word->load(butil::memory_order_relaxed); if ((l >> 31) == 0) { // Refuse to increment when the reader count has saturated // the low 31 bits. Otherwise `l + 1' would flip bit 31 and // we would corrupt lock_word into "writer held" state. // POSIX-style: report EAGAIN ("max read locks exceeded"). if (BAIDU_UNLIKELY(l == 0x7FFFFFFFu)) { LOG(ERROR) << "Too many readers on bthread_rwlock_t=" << rwlock; rc = EAGAIN; break; } // Acquire on success synchronizes-with the release-CAS in // unwrlock(), so any data written by the previous writer is // visible to us before we start reading. if (lock_word->compare_exchange_weak(l, l + 1, butil::memory_order_acquire, butil::memory_order_relaxed)) { rc = 0; break; } // CAS failed (likely another reader bumped r): retry. } else if (try_lock) { // Write lock is currently held. return EBUSY; } else { // Write lock currently held but not yet self-accounted as a // pending writer (very narrow window inside wrlock). Arm // sampling now so the spin/wait until writer_wait_count >= 1 // is also accounted for. BTHREAD_RWLOCK_MAYBE_START_SAMPLING; } // Otherwise (write lock held but not try_lock): spin once more. // The next iteration will observe writer_wait_count >= 1 (writers // self-account in writer_wait_count for the entire wrlock lifetime), // and we will block on it instead of busy spinning. } // Submit one contention sample for this reader (success or failure). submit_contention_if_sampled(start_ns, sampling_range); return rc; } static int rwlock_unrdlock(bthread_rwlock_t* rwlock) { auto lock_word = (butil::atomic*)rwlock->lock_word; while (true) { unsigned l = lock_word->load(butil::memory_order_relaxed); // Misuse detection: the caller must currently hold a read lock. // l == 0 -> no lock is held (double unlock?) // (l >> 31) != 0 -> write lock is held, not read lock if (l == 0 || (l >> 31) != 0) { LOG(ERROR) << "Invalid unrdlock on bthread_rwlock_t=" << rwlock << ", lock_word=" << l; return EINVAL; } // Release on success publishes any reads/writes done while holding // the read lock to the next acquirer (typically a writer's // acquire-CAS in wrlock()). if(!(lock_word->compare_exchange_weak(l, l - 1, butil::memory_order_release, butil::memory_order_relaxed))) { continue; } // We were the last reader (lock_word transitioned 1 -> 0). Wake the // single writer (if any) that may be sleeping on `lock_word' inside // wrlock(). At most one writer can be there because writers are // serialized by writer_queue_mutex. // No-op if nobody is waiting; butex_wake() short-circuits cheaply. if (l == 1) { butex_wake(lock_word); } return 0; } } // Roll back the side effects of a failed wrlock attempt: // - Release writer_queue_mutex if we managed to acquire it. // - Decrement our share of writer_wait_count. // - If we were the last in-flight writer, wake all readers that have // been parked by writer-priority (w == 1 means writer_wait_count is now 0). // Called on EBUSY (try_lock failed), ETIMEDOUT, EINTR-leading-to-fail. static BUTIL_FORCE_INLINE void rwlock_wrlock_cleanup(bthread_rwlock_t* rwlock, bool write_queue_locked) { if (write_queue_locked) { bthread_mutex_unlock(&rwlock->writer_queue_mutex); } auto writer_wait_count = (butil::atomic*)rwlock->writer_wait_count; // Withdraw our writer-priority "vote" so readers can make progress. auto w = writer_wait_count->fetch_sub(1, butil::memory_order_relaxed); // w is the value BEFORE the subtraction, so w == 1 means we were the // last writer in flight; wake every reader parked on writer_wait_count. if (w == 1) { butex_wake_all(writer_wait_count); } } static int rwlock_wrlock(bthread_rwlock_t* rwlock, bool try_lock, const struct timespec* abstime) { auto writer_wait_count = (butil::atomic*)rwlock->writer_wait_count; // Step 1: announce ourselves before doing anything else, so that // concurrent readers immediately observe writer-priority and back off. // This MUST happen before we try to acquire writer_queue_mutex, // otherwise a flood of readers could starve us indefinitely. // 2^31 in-flight writers should be enough for any realistic workload. writer_wait_count->fetch_add(1, butil::memory_order_relaxed); // Sampling state for the contention profiler. Both wrlock() and // unwrlock() sample independently: wrlock() submits its own wait time // on the way out (success or failure); unwrlock() samples its own // CAS-spin / mutex_unlock / butex_wake_all latency separately. We do // NOT use rwlock->writer_csite here -- the two operations are not // forced to share a single sample. size_t sampling_range = bvar::INVALID_SAMPLING_RANGE; int64_t start_ns = 0; // Step 2: serialize with other writers. At most one writer holds // `writer_queue_mutex' at a time and races for `lock_word'. int rc = bthread_mutex_trylock(&rwlock->writer_queue_mutex); if (0 != rc) { if (try_lock) { // Fail to acquire the wrlock. Don't sample trywrlock failures: // they are by design a non-blocking probe, not a contention event. rwlock_wrlock_cleanup(rwlock, false); return rc; } // We are about to block on writer_queue_mutex; arm sampling. // Note: the inner mutex itself has csite disabled (see init), so // its blocking time is only counted once -- here, by the rwlock. BTHREAD_RWLOCK_MAYBE_START_SAMPLING; rc = bthread_mutex_timedlock(&rwlock->writer_queue_mutex, abstime); if (0 != rc) { // Fail to acquire the wrlock. Submit the elapsed wait time // directly (no unwrlock() will run for this writer). submit_contention_if_sampled(start_ns, sampling_range); rwlock_wrlock_cleanup(rwlock, false); return rc; } } // Step 3: with `writer_queue_mutex' held, wait for all readers to drain // and then claim the write bit of `lock_word'. auto lock_word = (butil::atomic*)rwlock->lock_word; while (true) { unsigned l = lock_word->load(butil::memory_order_relaxed); if (l != 0) { // Readers still hold the lock. Park on `lock_word' until the last // reader releases (unrdlock will butex_wake on transition 1->0). if (try_lock) { errno = EBUSY; break; } // Arm sampling before parking so the wait-for-readers time is // counted (in case the queue_mutex acquisition above was uncontended). BTHREAD_RWLOCK_MAYBE_START_SAMPLING; // Use the freshly read `r' as expected; if lock_word changes // before we sleep, butex_wait returns EWOULDBLOCK and we retry. if (butex_wait(lock_word, l, abstime) < 0 && errno != EWOULDBLOCK && errno != EINTR) { break; } continue; } // Acquire on success synchronizes-with release-CAS in // unrdlock()/unwrlock(): we will see all data published by the // previous reader/writer before we start writing. if (lock_word->compare_exchange_weak(l, (unsigned)(1 << 31), butil::memory_order_acquire, butil::memory_order_relaxed)) { // Submit the writer's wait sample immediately on success. // unwrlock() will sample its own latency separately. submit_contention_if_sampled(start_ns, sampling_range); return 0; } // CAS may spuriously fail (weak); retry without sleeping. } // Failure path: snapshot errno before cleanup, because // bthread_mutex_unlock / butex_wake_all inside cleanup may invoke // syscalls or yield and clobber errno on this thread. int saved_errno = errno; // Submit the elapsed wait directly; we never reached unwrlock(). submit_contention_if_sampled(start_ns, sampling_range); rwlock_wrlock_cleanup(rwlock, true); return saved_errno; } static int rwlock_unwrlock(bthread_rwlock_t* rwlock) { auto lock_word = (butil::atomic*)rwlock->lock_word; auto writer_wait_count = (butil::atomic*)rwlock->writer_wait_count; // Sampling state for the contention profiler. unwrlock() samples // independently of wrlock(): although the release-CAS itself cannot // fail due to writer-writer contention (writers are serialized by // writer_queue_mutex), the body still does mutex_unlock(), // butex_wake_all() and may spuriously spin on the weak CAS, all of // which contribute to the critical-section tail latency. size_t sampling_range = bvar::INVALID_SAMPLING_RANGE; int64_t start_ns = 0; BTHREAD_RWLOCK_MAYBE_START_SAMPLING; while (true) { unsigned l = lock_word->load(butil::memory_order_relaxed); // Misuse detection: we must currently hold the write lock. if (BAIDU_UNLIKELY(l != (unsigned)(1 << 31))) { LOG(ERROR) << "Invalid unwrlock!"; return EINVAL; } // Release-CAS publishes all writes performed under the write lock // to the next acquirer (a reader's acquire-CAS or another writer's // acquire-CAS). The CAS itself cannot fail due to contention since // writers are serialized by writer_queue_mutex; weak failure here is // only a spurious CAS failure -- just retry. if (!lock_word->compare_exchange_weak(l, 0, butil::memory_order_release, butil::memory_order_relaxed)) { continue; } // ---- Order of the next two operations is INTENTIONAL ---- // // We deliberately: // (1) unlock writer_queue_mutex FIRST, then // (2) fetch_sub(writer_wait_count) and conditionally wake readers. // // Rationale (writer-priority semantics): // * Any writer queued on writer_queue_mutex has already // fetch_add'ed its share into writer_wait_count back in wrlock() // (before it even tried to lock the mutex). So when it wakes // up here and we later fetch_sub, the counter still reflects // "there is at least one more writer in flight": w_old >= 2, // which means w != 1, which means we will NOT wake readers. // Readers must keep yielding to the next writer -- exactly the // writer-priority invariant. // * Only when we are truly the last writer in flight (w_old == 1 // after our fetch_sub, i.e. writer_wait_count is now 0) do we // wake_all readers parked on writer_wait_count. // // Subtle but harmless effect: // Between (1) and (2) there is a small window in which our // own "ghost share" is still counted in writer_wait_count even though // we have effectively left. New readers entering rdlock() during // this window will see writer_wait_count >= 1 and park on it; they // will be woken either by step (2) below (if no successor writer // appeared) or by the successor writer's eventual unwrlock. // No wakeup is ever lost: butex_wait re-checks the expected // value before truly sleeping, and any successor writer will // itself execute this same wake logic on its way out. // // Reversing the order (fetch_sub before unlock mutex) would break // strict writer-priority because woken readers could grab the // read lock before a successor writer queued on the mutex even // gets a chance to CAS lock_word. bthread_mutex_unlock(&rwlock->writer_queue_mutex); unsigned w = writer_wait_count->fetch_sub(1, butil::memory_order_relaxed); if (w == 1) { butex_wake_all(writer_wait_count); } // Submit our own unwrlock-side sample (CAS spin + mutex_unlock + // butex_wake_all). This is independent of the wrlock-side sample. submit_contention_if_sampled(start_ns, sampling_range); return 0; } } // Generic unlock entry that dispatches to unwrlock/unrdlock by inspecting // `lock_word'. This is safe ONLY because the caller must already hold one of // the two locks: while holding a read lock the high bit of `lock_word' cannot // flip on, and while holding the write lock the low bits cannot be set. // Therefore a relaxed load is sufficient to make the dispatch decision. static int rwlock_unlock(bthread_rwlock_t* rwlock) { auto lock_word = (butil::atomic*)rwlock->lock_word; unsigned r = lock_word->load(butil::memory_order_relaxed); if ((r >> 31) != 0) { return rwlock_unwrlock(rwlock); } else { return rwlock_unrdlock(rwlock); } } // Deleter that turns butex_create_checked()'s raw pointer into something // std::unique_ptr can clean up automatically. Using RAII here lets the // init-error paths just `return rc' without manually unwinding partial // allocations; ownership is `release()'d only on the all-success path. struct ButexDeleter { void operator()(void* butex) const { if (butex != nullptr) { butex_destroy(butex); } } }; static int rwlock_init(bthread_rwlock_t* rwlock) { std::unique_ptr writer_wait_count( butex_create_checked()); if (writer_wait_count == nullptr) { LOG(ERROR) << "Fail to create writer_wait_count butex: out of memory"; return ENOMEM; } std::unique_ptr lock_word(butex_create_checked()); if (lock_word == nullptr) { LOG(ERROR) << "Fail to create lock_word butex: out of memory"; return ENOMEM; } *writer_wait_count = 0; *lock_word = 0; bthread_mutexattr_t attr; bthread_mutexattr_init(&attr); BRPC_SCOPE_EXIT { bthread_mutexattr_destroy(&attr); }; // Disable csite on the inner queue mutex so the writer's wait time is // accounted exactly once -- by the rwlock layer, not double-counted via // the inner mutex. bthread_mutexattr_disable_csite(&attr); const int rc = bthread_mutex_init(&rwlock->writer_queue_mutex, &attr); if (rc != 0) { LOG(ERROR) << "Fail to init writer_queue_mutex, rc=" << rc; return rc; } // All resources successfully created; transfer butex ownership to // rwlock. From here on, bthread_rwlock_destroy() is responsible for // releasing them. rwlock->writer_wait_count = writer_wait_count.release(); rwlock->lock_word = lock_word.release(); return 0; } static int rwlock_destroy(bthread_rwlock_t* rwlock) { // Destroy the inner mutex first; bthread_mutex_init() allocates an // internal butex which would otherwise leak. Pointers are nulled to // surface accidental double-destroy / use-after-destroy bugs early. int rc = bthread_mutex_destroy(&rwlock->writer_queue_mutex); if (rc != 0) { LOG(ERROR) << "Fail to destroy writer_queue_mutex, rc=" << rc; } if (rwlock->writer_wait_count != nullptr) { butex_destroy(rwlock->writer_wait_count); rwlock->writer_wait_count = nullptr; } if (rwlock->lock_word != nullptr) { butex_destroy(rwlock->lock_word); rwlock->lock_word = nullptr; } return rc; } } // namespace bthread __BEGIN_DECLS int bthread_rwlock_init(bthread_rwlock_t* __restrict rwlock, const bthread_rwlockattr_t* __restrict) { return bthread::rwlock_init(rwlock); } int bthread_rwlock_destroy(bthread_rwlock_t* rwlock) { return bthread::rwlock_destroy(rwlock); } int bthread_rwlock_rdlock(bthread_rwlock_t* rwlock) { return bthread::rwlock_rdlock(rwlock, false, nullptr); } int bthread_rwlock_tryrdlock(bthread_rwlock_t* rwlock) { return bthread::rwlock_rdlock(rwlock, true, nullptr); } int bthread_rwlock_timedrdlock(bthread_rwlock_t* __restrict rwlock, const struct timespec* __restrict abstime) { return bthread::rwlock_rdlock(rwlock, false, abstime); } int bthread_rwlock_wrlock(bthread_rwlock_t* rwlock) { return bthread::rwlock_wrlock(rwlock, false, nullptr); } int bthread_rwlock_trywrlock(bthread_rwlock_t* rwlock) { return bthread::rwlock_wrlock(rwlock, true, nullptr); } int bthread_rwlock_timedwrlock(bthread_rwlock_t* __restrict rwlock, const struct timespec* __restrict abstime) { return bthread::rwlock_wrlock(rwlock, false, abstime); } int bthread_rwlock_unlock(bthread_rwlock_t* rwlock) { return bthread::rwlock_unlock(rwlock); } __END_DECLS