Line data Source code
1 0 : // Distributed under the MIT License. 2 : // See LICENSE.txt for details. 3 : 4 : #pragma once 5 : 6 : #include <atomic> 7 : #include <limits> 8 : #include <new> // for hardware_destructive_interference_size 9 : 10 : namespace Parallel { 11 : /*! 12 : * \brief A two-state spinlock that allows multiple readers of a shared resource 13 : * to acquire the lock simultaneously. 14 : * 15 : * A spinlock (i.e. a non-yielding lock) that can be used to guard a resource 16 : * where multiple threads can safely perform some types of operations 17 : * (e.g. reader entries from a container), while only one thread can safely 18 : * perform other types of operations (e.g. modifying the container). 19 : * 20 : * ### Implementation 21 : * We lock by using a `std::atomic<std::int64_t>` (a signed integer). The lock 22 : * is not locked if the integer is `0`. It is read-locked if it is positive 23 : * and write-locked if it is negative. To read-lock we first check that the 24 : * integer is non-negative and then `fetch_add(1, std::memory_order_acq_rel)`, 25 : * then check that the number _before_ the increment is non-negative. The 26 : * reason for the second check is that between checking that the lock is not 27 : * write-locked and the `fetch_add()` operation, it could be write-locked by 28 : * another thread. To write-lock, we check that the integer is `0` and if so 29 : * we set it to `std::numeric_limits<std::int64_t>::lowest()` (i.e. `-2^{63}`). 30 : * We achieve this by using a `compare_exchange_strong()` with failure memory 31 : * order `relaxed` and success memory order `acq_rel`. This guarantees that 32 : * different threads synchronize using the lock. 33 : * 34 : * A thread read-unlocks the lock using a 35 : * `fetch_sub(1, std::memory_order_acq_rel)` operation. 36 : * 37 : * A thread write-unlocks the lock using a `store(0, std::memory_order_release)` 38 : * operation. 39 : */ 40 1 : class MultiReaderSpinlock { 41 : public: 42 0 : MultiReaderSpinlock() = default; 43 0 : MultiReaderSpinlock(const MultiReaderSpinlock&) = delete; 44 0 : MultiReaderSpinlock& operator=(const MultiReaderSpinlock&) = delete; 45 0 : MultiReaderSpinlock(MultiReaderSpinlock&&) = delete; 46 0 : MultiReaderSpinlock& operator=(MultiReaderSpinlock&&) = delete; 47 0 : ~MultiReaderSpinlock() = default; 48 : 49 : /// \brief Acquire the lock in a reader state. 50 : /// 51 : /// \note Multiple threads can acquire a reader state simultaneously. 52 1 : void read_lock() noexcept { 53 : for (;;) { 54 : while (lock_.load(std::memory_order_relaxed) < 0) { 55 : } 56 : 57 : if (lock_.fetch_add(1, std::memory_order_acquire) > -1) { 58 : return; 59 : } 60 : } 61 : } 62 : 63 : /// \brief Release the lock from a reader state. 64 : /// 65 : /// \note Since multiple threads can simultaneously acquire a reader state, 66 : /// unlock from a single reader does not guarantee a writer can acquire the 67 : /// lock in a write state. 68 1 : void read_unlock() noexcept { lock_.fetch_sub(1, std::memory_order_release); } 69 : 70 : /// \brief Acquire the lock in a writer state. 71 : /// 72 : /// Once acquired, no other thread can acquire the lock in either a read or 73 : /// a write state until this thread unlocks it. 74 1 : void write_lock() noexcept { 75 : for (;;) { 76 : std::int64_t current_value{0}; 77 : if (lock_.compare_exchange_strong( 78 : current_value, std::numeric_limits<std::int64_t>::lowest(), 79 : std::memory_order_acq_rel, std::memory_order_relaxed)) { 80 : return; 81 : } 82 : 83 : while (lock_.load(std::memory_order_relaxed) != 0) { 84 : } 85 : } 86 : } 87 : 88 : /// \brief Release the lock from a reader state. 89 1 : void write_unlock() noexcept { lock_.store(0, std::memory_order_release); } 90 : 91 : private: 92 : #ifdef __cpp_lib_hardware_interference_size 93 : static constexpr size_t cache_line_size_ = 94 : std::hardware_destructive_interference_size; 95 : #else 96 0 : static constexpr size_t cache_line_size_ = 64; 97 : #endif 98 : 99 0 : alignas(cache_line_size_) std::atomic<std::int64_t> lock_{0}; 100 : // Ensure we pad the end to avoid false sharing. Unlikely to happen because 101 : // of the layout, but better to be safe. 102 : #if defined(__clang__) 103 : #pragma GCC diagnostic push 104 : #pragma GCC diagnostic ignored "-Wunused-private-field" 105 : #endif 106 : // NOLINTNEXTLINE(modernize-avoid-c-arrays) 107 0 : char padding_[cache_line_size_ - sizeof(std::atomic<std::int64_t>)] = {}; 108 : #if defined(__clang__) 109 : #pragma GCC diagnostic pop 110 : #endif 111 : }; 112 : } // namespace Parallel