Skip to content

Commit 4a684f6

Browse files
authored
Monitor class in 'util' library (#1415)
* This adds a new monitor class for simplified concurrency handling * New directory and namespace structure in 'util' lib Signed-off-by: Jan Christoph Habig <[email protected]>
1 parent 648a6fc commit 4a684f6

File tree

9 files changed

+599
-17
lines changed

9 files changed

+599
-17
lines changed

lib/everest/tls/include/everest/tls/tls_types.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
#include <cstdint>
88

9-
#include <everest/util/EnumFlags.hpp>
9+
#include <everest/util/enum/EnumFlags.hpp>
1010

1111
struct ocsp_response_st;
1212
struct ssl_ctx_st;
@@ -26,7 +26,7 @@ class StatusFlags {
2626
last = trusted_ca_keys,
2727
};
2828

29-
everest::util::AtomicEnumFlags<flags_t> flags;
29+
everest::lib::util::AtomicEnumFlags<flags_t> flags;
3030

3131
public:
3232
void status_request_received() {

lib/everest/tls/tests/tls_connection_test.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
#include <thread>
1818
#include <unistd.h>
1919

20-
#include <everest/util/EnumFlags.hpp>
20+
#include <everest/util/enum/EnumFlags.hpp>
2121

2222
using namespace std::chrono_literals;
2323

@@ -36,10 +36,10 @@ struct ClientStatusRequestV2Test : public ClientStatusRequestV2 {
3636
last = connected,
3737
};
3838

39-
everest::util::AtomicEnumFlags<flags_t>& flags;
39+
everest::lib::util::AtomicEnumFlags<flags_t>& flags;
4040

4141
ClientStatusRequestV2Test() = delete;
42-
explicit ClientStatusRequestV2Test(everest::util::AtomicEnumFlags<flags_t>& flag_ref) : flags(flag_ref) {
42+
explicit ClientStatusRequestV2Test(everest::lib::util::AtomicEnumFlags<flags_t>& flag_ref) : flags(flag_ref) {
4343
}
4444

4545
int status_request_cb(tls::Ssl* ctx) override {
@@ -97,7 +97,7 @@ struct ClientStatusRequestV2Test : public ClientStatusRequestV2 {
9797

9898
struct ClientTest : public tls::Client {
9999
using flags_t = ClientStatusRequestV2Test::flags_t;
100-
everest::util::AtomicEnumFlags<flags_t> flags;
100+
everest::lib::util::AtomicEnumFlags<flags_t> flags;
101101

102102
ClientTest() : tls::Client(std::unique_ptr<ClientStatusRequestV2>(new ClientStatusRequestV2Test(flags))) {
103103
}

lib/everest/util/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ add_library(everest_util INTERFACE)
22
add_library(everest::util ALIAS everest_util)
33
ev_register_library_target(everest_helpers)
44

5+
set_target_properties(everest_util PROPERTIES
6+
VERSION 0.0.1
7+
)
8+
9+
510
target_include_directories(everest_util
611
INTERFACE
712
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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

lib/everest/util/include/everest/util/EnumFlags.hpp renamed to lib/everest/util/include/everest/util/enum/EnumFlags.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
#include <limits>
1515
#include <type_traits>
1616

17-
namespace everest::util {
17+
namespace everest::lib::util {
1818

1919
/**
2020
* \brief templated class to use an enumeration as bit flags
@@ -239,6 +239,6 @@ template <typename T> struct EnumFlags : public EnumFlagsBase<T, SelectedUInt<T>
239239

240240
template <typename T> struct AtomicEnumFlags : public EnumFlagsBase<T, std::atomic<SelectedUInt<T>>> {};
241241

242-
} // namespace everest::util
242+
} // namespace everest::lib::util
243243

244244
#endif
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
add_executable(EnumFlagsTest
2-
EnumFlagsTest.cpp
3-
EnumFlagsTest_B.cpp
1+
add_executable(everest_util_tests
2+
enum/EnumFlagsTest.cpp
3+
enum/EnumFlagsTest_B.cpp
4+
async/monitor_tests.cpp
45
)
56

6-
target_link_libraries(EnumFlagsTest
7+
target_link_libraries(everest_util_tests
78
PRIVATE
89
GTest::gtest_main
910
everest::util
1011
)
1112

1213
include(GoogleTest)
13-
gtest_discover_tests(EnumFlagsTest)
14+
gtest_discover_tests(everest_util_tests)

0 commit comments

Comments
 (0)