SpECTRE Documentation Coverage Report
Current view: top level - Parallel - MultiReaderSpinlock.hpp Coverage Total Hit
Commit: 0d1d717af84a81fb1284a20c4013708fe85cc2f0 Lines: 33.3 % 15 5
Test Date: 2026-10-22 17:29:42
Legend: Lines:     hit not hit

            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
        

Generated by: LCOV version 2.0-1