/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef threading_ExclusiveData_h #define threading_ExclusiveData_h #include "mozilla/Maybe.h" #include "mozilla/OperatorNewExtensions.h" #include #include "threading/ConditionVariable.h" #include "threading/Mutex.h" namespace js { /** * [SMDOC] ExclusiveData API * * A mutual exclusion lock class. * * `ExclusiveData` provides an RAII guard to automatically lock and unlock when * accessing the protected inner value. * * Unlike the STL's `std::mutex`, the protected value is internal to this * class. This is a huge win: one no longer has to rely on documentation to * explain the relationship between a lock and its protected data, and the type * system can enforce[0] it. * * For example, suppose we have a counter class: * * class Counter * { * int32_t i; * * public: * void inc(int32_t n) { i += n; } * }; * * If we share a counter across threads with `std::mutex`, we rely solely on * comments to document the relationship between the lock and its data, like * this: * * class SharedCounter * { * // Remember to acquire `counter_lock` when accessing `counter`, * // pretty please! * Counter counter; * std::mutex counter_lock; * * public: * void inc(size_t n) { * // Whoops, forgot to acquire the lock! Off to the races! * counter.inc(n); * } * }; * * In contrast, `ExclusiveData` wraps the protected value, enabling the type * system to enforce that we acquire the lock before accessing the value: * * class SharedCounter * { * ExclusiveData counter; * * public: * void inc(size_t n) { * auto guard = counter.lock(); * guard->inc(n); * } * }; * * The API design is based on Rust's `std::sync::Mutex` type. * * [0]: Of course, we don't have a borrow checker in C++, so the type system * cannot guarantee that you don't stash references received from * `ExclusiveData::Guard` somewhere such that the reference outlives the * guard's lifetime and therefore becomes invalid. To help avoid this last * foot-gun, prefer using the guard directly! Do not store raw references * to the protected value in other structures! */ template class ExclusiveData { protected: mutable Mutex lock_ MOZ_UNANNOTATED; mutable T value_; ExclusiveData(const ExclusiveData&) = delete; ExclusiveData& operator=(const ExclusiveData&) = delete; void acquire() const { lock_.lock(); } void release() const { lock_.unlock(); } public: /** * Create a new `ExclusiveData`, with perfect forwarding of the protected * value. */ template explicit ExclusiveData(const MutexId& id, U&& u) : lock_(id), value_(std::forward(u)) {} /** * Create a new `ExclusiveData`, constructing the protected value in place. */ template explicit ExclusiveData(const MutexId& id, Args&&... args) : lock_(id), value_(std::forward(args)...) {} ExclusiveData(ExclusiveData&& rhs) : lock_(std::move(rhs.lock)), value_(std::move(rhs.value_)) { MOZ_ASSERT(&rhs != this, "self-move disallowed!"); } ExclusiveData& operator=(ExclusiveData&& rhs) { this->~ExclusiveData(); new (mozilla::KnownNotNull, this) ExclusiveData(std::move(rhs)); return *this; } /** * An RAII class that provides exclusive access to a `ExclusiveData`'s * protected inner `T` value. * * Note that this is intentionally marked MOZ_STACK_CLASS instead of * MOZ_RAII_CLASS, as the latter disallows moves and returning by value, but * Guard utilizes both. */ class MOZ_STACK_CLASS Guard { protected: const ExclusiveData* parent_; explicit Guard(std::nullptr_t) : parent_(nullptr) {} private: Guard(const Guard&) = delete; Guard& operator=(const Guard&) = delete; public: explicit Guard(const ExclusiveData& parent) : parent_(&parent) { parent_->acquire(); } Guard(Guard&& rhs) : parent_(rhs.parent_) { MOZ_ASSERT(&rhs != this, "self-move disallowed!"); rhs.parent_ = nullptr; } Guard& operator=(Guard&& rhs) { this->~Guard(); new (this) Guard(std::move(rhs)); return *this; } T& get() const { MOZ_ASSERT(parent_); return parent_->value_; } operator T&() const { return get(); } T* operator->() const { return &get(); } const ExclusiveData* parent() const { MOZ_ASSERT(parent_); return parent_; } ~Guard() { if (parent_) { parent_->release(); } } }; /** * NullableGuard are similar to Guard, except that one the access to the * ExclusiveData might not always be granted. This is useful when contextual * information is enough to prevent useless use of Mutex. * * The NullableGuard can be manipulated as follows: * * if (NullableGuard guard = data.mightAccess()) { * // NullableGuard is acquired. * guard->... * } * // NullableGuard was either not acquired or released. * * Where mightAccess returns either a NullableGuard from `noAccess()` or a * Guard from `lock()`. */ class MOZ_STACK_CLASS NullableGuard : public Guard { public: explicit NullableGuard(std::nullptr_t) : Guard((std::nullptr_t) nullptr) {} explicit NullableGuard(const ExclusiveData& parent) : Guard(parent) {} explicit NullableGuard(Guard&& rhs) : Guard(std::move(rhs)) {} NullableGuard& operator=(Guard&& rhs) { this->~NullableGuard(); new (this) NullableGuard(std::move(rhs)); return *this; } /** * Returns whether this NullableGuard has access to the exclusive data. */ bool hasAccess() const { return this->parent_; } explicit operator bool() const { return hasAccess(); } }; /** * Access the protected inner `T` value for exclusive reading and writing. */ Guard lock() const { return Guard(*this); } /** * Provide a no-access guard, which coerces to false when tested. This value * can be returned if the guard access is conditioned on external factors. * * See NullableGuard. */ NullableGuard noAccess() const { return NullableGuard((std::nullptr_t) nullptr); } }; template class ExclusiveWaitableData : public ExclusiveData { using Base = ExclusiveData; mutable ConditionVariable condVar_; public: template explicit ExclusiveWaitableData(const MutexId& id, U&& u) : Base(id, std::forward(u)) {} template explicit ExclusiveWaitableData(const MutexId& id, Args&&... args) : Base(id, std::forward(args)...) {} class MOZ_STACK_CLASS Guard : public ExclusiveData::Guard { using Base = typename ExclusiveData::Guard; public: explicit Guard(const ExclusiveWaitableData& parent) : Base(parent) {} Guard(Guard&& guard) : Base(std::move(guard)) {} Guard& operator=(Guard&& rhs) { return Base::operator=(std::move(rhs)); } void wait() { auto* parent = static_cast(this->parent()); parent->condVar_.wait(parent->lock_); } void notify_one() { auto* parent = static_cast(this->parent()); parent->condVar_.notify_one(); } void notify_all() { auto* parent = static_cast(this->parent()); parent->condVar_.notify_all(); } }; Guard lock() const { return Guard(*this); } }; /** * Multiple-readers / single-writer variant of ExclusiveData. * * Readers call readLock() to obtain a stack-only RAII reader lock, which will * allow other readers to read concurrently but block writers; the yielded value * is const. Writers call writeLock() to obtain a ditto writer lock, which * yields exclusive access to non-const data. * * See ExclusiveData and its implementation for more documentation. */ template class RWExclusiveData { mutable Mutex lock_ MOZ_UNANNOTATED; mutable ConditionVariable cond_; mutable T value_; mutable int readers_; // We maintain a count of active readers. Writers may enter the critical // section only when the reader count is zero, so the reader that decrements // the count to zero must wake up any waiting writers. // // There can be multiple writers waiting, so a writer leaving the critical // section must also wake up any other waiting writers. void acquireReaderLock() const { lock_.lock(); readers_++; lock_.unlock(); } void releaseReaderLock() const { lock_.lock(); MOZ_ASSERT(readers_ > 0); if (--readers_ == 0) { cond_.notify_all(); } lock_.unlock(); } void acquireWriterLock() const { lock_.lock(); while (readers_ > 0) { cond_.wait(lock_); } } void releaseWriterLock() const { cond_.notify_all(); lock_.unlock(); } public: RWExclusiveData(const RWExclusiveData&) = delete; RWExclusiveData& operator=(const RWExclusiveData&) = delete; /** * Create a new `RWExclusiveData`, constructing the protected value in place. */ template explicit RWExclusiveData(const MutexId& id, Args&&... args) : lock_(id), value_(std::forward(args)...), readers_(0) {} class MOZ_STACK_CLASS ReadGuard { const RWExclusiveData* parent_; explicit ReadGuard(std::nullptr_t) : parent_(nullptr) {} public: ReadGuard(const ReadGuard&) = delete; ReadGuard& operator=(const ReadGuard&) = delete; explicit ReadGuard(const RWExclusiveData& parent) : parent_(&parent) { parent_->acquireReaderLock(); } ReadGuard(ReadGuard&& rhs) : parent_(rhs.parent_) { MOZ_ASSERT(&rhs != this, "self-move disallowed!"); rhs.parent_ = nullptr; } ReadGuard& operator=(ReadGuard&& rhs) { this->~ReadGuard(); new (this) ReadGuard(std::move(rhs)); return *this; } const T& get() const { MOZ_ASSERT(parent_); return parent_->value_; } operator const T&() const { return get(); } const T* operator->() const { return &get(); } const RWExclusiveData* parent() const { MOZ_ASSERT(parent_); return parent_; } ~ReadGuard() { if (parent_) { parent_->releaseReaderLock(); } } }; class MOZ_STACK_CLASS WriteGuard { const RWExclusiveData* parent_; explicit WriteGuard(std::nullptr_t) : parent_(nullptr) {} public: WriteGuard(const WriteGuard&) = delete; WriteGuard& operator=(const WriteGuard&) = delete; explicit WriteGuard(const RWExclusiveData& parent) : parent_(&parent) { parent_->acquireWriterLock(); } WriteGuard(WriteGuard&& rhs) : parent_(rhs.parent_) { MOZ_ASSERT(&rhs != this, "self-move disallowed!"); rhs.parent_ = nullptr; } WriteGuard& operator=(WriteGuard&& rhs) { this->~WriteGuard(); new (this) WriteGuard(std::move(rhs)); return *this; } T& get() const { MOZ_ASSERT(parent_); return parent_->value_; } operator T&() const { return get(); } T* operator->() const { return &get(); } const RWExclusiveData* parent() const { MOZ_ASSERT(parent_); return parent_; } ~WriteGuard() { if (parent_) { parent_->releaseWriterLock(); } } }; ReadGuard readLock() const { return ReadGuard(*this); } WriteGuard writeLock() const { return WriteGuard(*this); } }; } // namespace js #endif // threading_ExclusiveData_h