|  | 
|  | 1 | +// SPDX-License-Identifier: Apache-2.0 | 
|  | 2 | +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest | 
|  | 3 | + | 
|  | 4 | +/** | 
|  | 5 | + * @file monitor.hpp | 
|  | 6 | + * @brief Provides a generic RAII Monitor pattern implementation for thread-safe access to a shared resource. | 
|  | 7 | + * | 
|  | 8 | + * The Monitor pattern bundles shared data with a synchronization mechanism (mutex and condition variable) | 
|  | 9 | + * to ensure only one thread can access the data at any given time, and provides tools for thread | 
|  | 10 | + * coordination (waiting and signaling). | 
|  | 11 | + */ | 
|  | 12 | + | 
|  | 13 | +#pragma once | 
|  | 14 | + | 
|  | 15 | +#include <condition_variable> | 
|  | 16 | +#include <mutex> | 
|  | 17 | +#include <optional> | 
|  | 18 | +#include <type_traits> | 
|  | 19 | +#include <utility> | 
|  | 20 | + | 
|  | 21 | +namespace everest::lib::util { | 
|  | 22 | + | 
|  | 23 | +template <typename T, typename = void> struct has_arrow_operator : std::false_type {}; | 
|  | 24 | + | 
|  | 25 | +template <typename T> | 
|  | 26 | +struct has_arrow_operator<T, std::void_t<decltype(std::declval<T>().operator->())>> : std::true_type {}; | 
|  | 27 | + | 
|  | 28 | +template <typename T> inline constexpr bool has_arrow_operator_v = has_arrow_operator<T>::value; | 
|  | 29 | + | 
|  | 30 | +/** | 
|  | 31 | + * @brief The RAII guard that provides locked access to the shared data T. | 
|  | 32 | + * * This object is non-copyable but movable. Its existence guarantees that the | 
|  | 33 | + * underlying mutex in the parent monitor object is held. When this handle | 
|  | 34 | + * goes out of scope, the lock is automatically released. | 
|  | 35 | + * | 
|  | 36 | + * @tparam T The type of the protected resource. | 
|  | 37 | + * @tparam MTX The mutex type used for locking (e.g., std::mutex, std::recursive_mutex). | 
|  | 38 | + */ | 
|  | 39 | +template <class T, class MTX> class monitor_handle { | 
|  | 40 | +public: | 
|  | 41 | +    /** | 
|  | 42 | +     * @brief Constructs the monitor handle and takes ownership of the acquired lock. | 
|  | 43 | +     * @param obj Reference to the protected resource within the monitor. | 
|  | 44 | +     * @param mtx R-value reference to the acquired unique lock, ownership is moved. | 
|  | 45 | +     * @param cv Reference to the condition variable in the monitor. | 
|  | 46 | +     */ | 
|  | 47 | +    monitor_handle(T& obj, std::unique_lock<MTX>&& mtx, std::condition_variable& cv) : | 
|  | 48 | +        m_obj(obj), m_lock(std::move(mtx)), m_cv(cv) { | 
|  | 49 | +    } | 
|  | 50 | + | 
|  | 51 | +    // Monitor handles should not be copied, as that would duplicate a unique lock. | 
|  | 52 | +    monitor_handle(monitor_handle<T, MTX> const& other) = delete; | 
|  | 53 | +    monitor_handle<T, MTX>& operator=(monitor_handle<T, MTX> const& rhs) = delete; | 
|  | 54 | + | 
|  | 55 | +    // Defaulted move operations allow the handle to be moved (e.g., returned from monitor::handle). | 
|  | 56 | +    /** @brief generated default move constructor*/ | 
|  | 57 | +    monitor_handle(monitor_handle<T, MTX>&& other) = default; | 
|  | 58 | +    /** @brief generated default move assignment*/ | 
|  | 59 | +    monitor_handle<T, MTX>& operator=(monitor_handle<T, MTX>&& rhs) = default; | 
|  | 60 | + | 
|  | 61 | +    /** | 
|  | 62 | +     * @brief Destructor. Automatically releases the lock held by m_lock. | 
|  | 63 | +     */ | 
|  | 64 | +    ~monitor_handle() = default; | 
|  | 65 | + | 
|  | 66 | +    /** | 
|  | 67 | +     * @brief Overloads the dereference operator to allow reference access to the protected object. | 
|  | 68 | +     * * This provides direct reference access to the guarded object T (or the wrapper T, e.g., std::unique_ptr<...>&). | 
|  | 69 | +     * The lock is held during the access. | 
|  | 70 | +     * | 
|  | 71 | +     * @return Reference to the protected object T. | 
|  | 72 | +     */ | 
|  | 73 | +    T& operator*() { | 
|  | 74 | +        return m_obj; | 
|  | 75 | +    } | 
|  | 76 | + | 
|  | 77 | +    /** | 
|  | 78 | +     * @brief Overloads the arrow operator to provide unified pointer-like access to the protected object. | 
|  | 79 | +     * * This implementation uses C++17's `if constexpr` to support three primary access patterns: | 
|  | 80 | +     * 1. **Chaining (Returns T&):** Used when T is a pointer-like wrapper (e.g., std::unique_ptr<T>) or a raw pointer | 
|  | 81 | +     * (T*). This allows the compiler's built-in chaining mechanism to continue indirection until the final object is | 
|  | 82 | +     * reached. | 
|  | 83 | +     * 2. **Direct Pointer Access (Returns T*):** Used when T is the final object type (e.g., a simple struct or class). | 
|  | 84 | +     * This terminates the chain immediately with a pointer to the object. | 
|  | 85 | +     * * @note This method holds the mutex lock for the duration of the access. | 
|  | 86 | +     * | 
|  | 87 | +     * @return `decltype(auto)` returns T& for chaining/pointers, or T* for direct access. | 
|  | 88 | +     */ | 
|  | 89 | +    decltype(auto) operator->() { | 
|  | 90 | +        if constexpr (has_arrow_operator_v<T> || std::is_pointer_v<T>) { | 
|  | 91 | +            return m_obj; | 
|  | 92 | +        } else { | 
|  | 93 | +            return &m_obj; | 
|  | 94 | +        } | 
|  | 95 | +    } | 
|  | 96 | + | 
|  | 97 | +    /** | 
|  | 98 | +     * @brief Blocks the thread until the provided predicate returns true. | 
|  | 99 | +     * @details This function atomically releases the lock (allowing other threads to acquire it) | 
|  | 100 | +     * and waits for a notification on the condition variable. When woken, it re-acquires | 
|  | 101 | +     * the lock and re-checks the predicate. | 
|  | 102 | +     * * @note This method is only available when MTX is std::mutex. | 
|  | 103 | +     * @tparam Predicate The callable type (e.g., lambda) that returns a boolean. | 
|  | 104 | +     * @param pred The condition that must become true to stop waiting. | 
|  | 105 | +     */ | 
|  | 106 | +    template <class Predicate, class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> | 
|  | 107 | +    void wait(Predicate&& pred) { | 
|  | 108 | +        m_cv.wait(m_lock, std::forward<Predicate>(pred)); | 
|  | 109 | +    } | 
|  | 110 | + | 
|  | 111 | +    /** | 
|  | 112 | +     * @brief Blocks the thread until the predicate returns true or the timeout expires. | 
|  | 113 | +     * @note This method is only available when MTX is std::mutex. | 
|  | 114 | +     * @tparam Rep The type representing the duration count (e.g., int, long). | 
|  | 115 | +     * @tparam Period The type representing the duration period (e.g., std::milli, std::ratio<1>). | 
|  | 116 | +     * @tparam Predicate The callable type (e.g., lambda) that returns a boolean. | 
|  | 117 | +     * @param pred The condition that must become true to stop waiting. | 
|  | 118 | +     * @param timeout The maximum time to wait for the condition to become true. | 
|  | 119 | +     * @return true if the predicate became true, false if the timeout expired. | 
|  | 120 | +     */ | 
|  | 121 | +    template <class Rep, class Period, class Predicate, class U = MTX, | 
|  | 122 | +              std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> | 
|  | 123 | +    bool wait_for(Predicate&& pred, std::chrono::duration<Rep, Period> timeout) { | 
|  | 124 | +        return m_cv.wait_for(m_lock, timeout, std::forward<Predicate>(pred)); | 
|  | 125 | +    } | 
|  | 126 | + | 
|  | 127 | +    /** | 
|  | 128 | +     * @brief Blocks the thread until the predicate returns true or the absolute time point is reached. | 
|  | 129 | +     * * If the predicate is false, the lock is atomically released, and the thread sleeps until | 
|  | 130 | +     * a notification is received or abs_time is reached. When woken, the lock is re-acquired | 
|  | 131 | +     * and the predicate is re-checked. | 
|  | 132 | +     * @note This method is only available when MTX is std::mutex. | 
|  | 133 | +     * @tparam Clock The clock type used for the time point (e.g., std::system_clock, std::steady_clock). | 
|  | 134 | +     * @tparam Duration The duration type used for the time point. | 
|  | 135 | +     * @tparam Predicate The callable type (e.g., lambda) that returns a boolean. | 
|  | 136 | +     * @param abs_time The absolute time point at which the wait will cease, regardless of predicate state. | 
|  | 137 | +     * @param pred The condition that must become true to stop waiting. | 
|  | 138 | +     * @return true if the predicate became true, false if the absolute time was reached. | 
|  | 139 | +     */ | 
|  | 140 | +    template <class Clock, class Duration, class Predicate, class U = MTX, | 
|  | 141 | +              std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> | 
|  | 142 | +    bool wait_until(std::chrono::time_point<Clock, Duration> const& abs_time, Predicate&& pred) { | 
|  | 143 | +        return m_cv.wait_until(m_lock, abs_time, std::forward<Predicate>(pred)); | 
|  | 144 | +    } | 
|  | 145 | + | 
|  | 146 | +private: | 
|  | 147 | +    T& m_obj;                      ///< Reference to the protected resource. | 
|  | 148 | +    std::unique_lock<MTX> m_lock;  ///< The unique lock, holding the mutex during the handle's lifetime. | 
|  | 149 | +    std::condition_variable& m_cv; ///< Reference to the monitor's condition variable. | 
|  | 150 | +}; | 
|  | 151 | + | 
|  | 152 | +/** | 
|  | 153 | + * @brief A generic monitor class that manages RAII access to its resource T. | 
|  | 154 | + * * Provides thread-safe data encapsulation using a mutex and thread coordination | 
|  | 155 | + * via a condition variable. Access to the internal resource T is only possible | 
|  | 156 | + * by obtaining a monitor_handle. | 
|  | 157 | + * | 
|  | 158 | + * @tparam T The type of the resource being protected. | 
|  | 159 | + * @tparam MTX The mutex type to use, defaults to std::mutex. | 
|  | 160 | + */ | 
|  | 161 | +template <class T, class MTX = std::mutex> class monitor { | 
|  | 162 | +public: | 
|  | 163 | +    monitor() = default; | 
|  | 164 | +    /** | 
|  | 165 | +     * @brief Constructs the internal object T using move construction. | 
|  | 166 | +     */ | 
|  | 167 | +    explicit monitor(T&& obj) : m_obj(std::move(obj)) { | 
|  | 168 | +    } | 
|  | 169 | + | 
|  | 170 | +    /** | 
|  | 171 | +     * @brief Constructs the internal object T using perfect forwarding. | 
|  | 172 | +     * @tparam ArgsT Types of arguments used to construct T. | 
|  | 173 | +     * @param args Arguments passed to the constructor of T. | 
|  | 174 | +     */ | 
|  | 175 | +    template <class... ArgsT> explicit monitor(ArgsT&&... args) : m_obj(std::forward<ArgsT>(args)...) { | 
|  | 176 | +    } | 
|  | 177 | + | 
|  | 178 | +    ~monitor() = default; | 
|  | 179 | + | 
|  | 180 | +    // Monitors should not be copied. | 
|  | 181 | +    monitor(monitor<T, MTX> const& other) = delete; | 
|  | 182 | +    monitor<T, MTX>& operator=(monitor<T, MTX> const& rhs) = delete; | 
|  | 183 | + | 
|  | 184 | +    /** | 
|  | 185 | +     * @brief Thread-safe move constructor. Locks the source mutex before swapping. | 
|  | 186 | +     */ | 
|  | 187 | +    monitor(monitor<T, MTX>&& other) { | 
|  | 188 | +        // Lock the source monitor's mutex before moving its data to ensure thread safety | 
|  | 189 | +        std::unique_lock lock(other.m_mtx); | 
|  | 190 | +        std::swap(m_obj, other.m_obj); | 
|  | 191 | +        // Note: m_mtx and m_cv are not swapped; they remain tied to the current object. | 
|  | 192 | +    } | 
|  | 193 | + | 
|  | 194 | +    /** | 
|  | 195 | +     * @brief Thread-safe move assignment operator. Locks the source mutex before swapping. | 
|  | 196 | +     * @return Reference to the current object. | 
|  | 197 | +     */ | 
|  | 198 | +    monitor<T, MTX>& operator=(monitor<T, MTX>&& rhs) { | 
|  | 199 | +        // Lock the source monitor's mutex before moving its data to ensure thread safety | 
|  | 200 | +        std::unique_lock lock(rhs.m_mtx); | 
|  | 201 | +        std::swap(m_obj, rhs.m_obj); | 
|  | 202 | +        return *this; | 
|  | 203 | +    } | 
|  | 204 | + | 
|  | 205 | +    /** | 
|  | 206 | +     * @brief Blocks indefinitely to acquire the lock and return a handle. | 
|  | 207 | +     * @return monitor_handle<T, MTX> with ownership of the acquired lock. | 
|  | 208 | +     */ | 
|  | 209 | +    monitor_handle<T, MTX> handle() { | 
|  | 210 | +        std::unique_lock lock(m_mtx); | 
|  | 211 | +        return monitor_handle<T, MTX>(m_obj, std::move(lock), m_cv); | 
|  | 212 | +    } | 
|  | 213 | + | 
|  | 214 | +    /** | 
|  | 215 | +     * @brief Attempts to acquire the lock within the specified timeout duration. | 
|  | 216 | +     * @note This method is only available when MTX is std::timed_mutex. | 
|  | 217 | +     * @tparam Rep The type representing the duration count. | 
|  | 218 | +     * @tparam Period The type representing the duration period. | 
|  | 219 | +     * @param timeout The maximum time to wait for the lock. | 
|  | 220 | +     * @return An optional handle: contains the handle if the lock was acquired, std::nullopt otherwise. | 
|  | 221 | +     */ | 
|  | 222 | +    template <class Rep, class Period, class U = MTX, std::enable_if_t<std::is_same_v<U, std::timed_mutex>>* = nullptr> | 
|  | 223 | +    std::optional<monitor_handle<T, MTX>> handle(std::chrono::duration<Rep, Period> timeout) { | 
|  | 224 | +        auto deadline = std::chrono::steady_clock::now() + timeout; | 
|  | 225 | + | 
|  | 226 | +        std::unique_lock lock(m_mtx, std::defer_lock); | 
|  | 227 | +        if (not lock.try_lock_until(deadline)) { | 
|  | 228 | +            return std::nullopt; | 
|  | 229 | +        } | 
|  | 230 | + | 
|  | 231 | +        return monitor_handle<T, MTX>(m_obj, std::move(lock), m_cv); | 
|  | 232 | +    } | 
|  | 233 | + | 
|  | 234 | +    /** | 
|  | 235 | +     * @brief Wakes up one thread currently waiting on the monitor's condition variable. | 
|  | 236 | +     * @note This method is only available when MTX is std::mutex. | 
|  | 237 | +     */ | 
|  | 238 | +    template <class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> void notify_one() { | 
|  | 239 | +        m_cv.notify_one(); | 
|  | 240 | +    } | 
|  | 241 | + | 
|  | 242 | +    /** | 
|  | 243 | +     * @brief Wakes up all threads currently waiting on the monitor's condition variable. | 
|  | 244 | +     * @note This method is only available when MTX is std::mutex. | 
|  | 245 | +     */ | 
|  | 246 | +    template <class U = MTX, std::enable_if_t<std::is_same_v<U, std::mutex>>* = nullptr> void notify_all() { | 
|  | 247 | +        m_cv.notify_all(); | 
|  | 248 | +    } | 
|  | 249 | + | 
|  | 250 | +private: | 
|  | 251 | +    MTX m_mtx;                    ///< The mutex protecting the resource T. | 
|  | 252 | +    T m_obj;                      ///< The protected resource. | 
|  | 253 | +    std::condition_variable m_cv; ///< The condition variable for thread coordination. | 
|  | 254 | +}; | 
|  | 255 | + | 
|  | 256 | +} // namespace everest::lib::util | 
0 commit comments