userver: userver/engine/semaphore.hpp Source File
Loading...
Searching...
No Matches
semaphore.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/engine/semaphore.hpp
4/// @brief @copybrief engine::Semaphore
5
6#include <atomic>
7#include <chrono>
8#include <shared_mutex> // for std locks
9#include <stdexcept>
10
11#include <userver/engine/deadline.hpp>
12#include <userver/engine/impl/wait_list_fwd.hpp>
13
14USERVER_NAMESPACE_BEGIN
15
16namespace engine {
17
18/// Thrown by engine::Semaphore when an amount of locks greater than its current
19/// capacity is requested.
20class UnreachableSemaphoreLockError final : public std::runtime_error {
21 public:
22 using std::runtime_error::runtime_error;
23};
24
25/// Thrown by engine::CancellableSemaphore on current task cancellation
26class SemaphoreLockCancelledError final : public std::runtime_error {
27 public:
28 using std::runtime_error::runtime_error;
29};
30
31/// @ingroup userver_concurrency
32///
33/// @brief Class that allows up to `max_simultaneous_locks` concurrent accesses
34/// to the critical section. It honours task cancellation, unlike Semaphore.
35///
36/// ## Example usage:
37///
38/// @snippet engine/semaphore_test.cpp Sample engine::Semaphore usage
39///
40/// @see @ref scripts/docs/en/userver/synchronization.md
41class CancellableSemaphore final {
42 public:
43 using Counter = std::size_t;
44
45 /// Creates a semaphore with predefined number of available locks
46 /// @param capacity initial number of available locks
47 explicit CancellableSemaphore(Counter capacity);
48
49 ~CancellableSemaphore();
50
51 CancellableSemaphore(CancellableSemaphore&&) = delete;
52 CancellableSemaphore(const CancellableSemaphore&) = delete;
53 CancellableSemaphore& operator=(CancellableSemaphore&&) = delete;
54 CancellableSemaphore& operator=(const CancellableSemaphore&) = delete;
55
56 /// Sets the total number of available locks. If the lock count decreases, the
57 /// current acquired lock count may temporarily go above the limit.
58 void SetCapacity(Counter capacity);
59
60 /// Gets the total number of available locks.
61 [[nodiscard]] Counter GetCapacity() const noexcept;
62
63 /// Returns an approximate number of available locks, use only for statistics.
64 [[nodiscard]] std::size_t RemainingApprox() const;
65
66 /// Returns an approximate number of used locks, use only for statistics.
67 [[nodiscard]] std::size_t UsedApprox() const;
68
69 /// Decrements internal semaphore lock counter. Blocks if current counter is
70 /// zero until the subsequent call to unlock_shared() by another coroutine.
71 ///
72 /// @note the user should eventually call unlock_shared() to increment the
73 /// lock counter.
74 ///
75 /// @note the method doesn't wait for the semaphore if the current task is
76 /// cancelled. If a task waits on CancellableSemaphore and the cancellation
77 /// is requested, the waiting is aborted with an exception.
78 ///
79 /// @throws UnreachableSemaphoreLockError if `capacity == 0`
80 /// @throws SemaphoreLockCancelledError if the current task is cancelled
82
83 /// Increments internal semaphore lock counter. If there is a user waiting in
84 /// lock_shared() on the same semaphore, it is woken up.
85 ///
86 /// @note the order of coroutines to unblock is unspecified. Any code assuming
87 /// any specific order (e.g. FIFO) is incorrect and must be fixed.
88 ///
89 /// @note it is allowed to call lock_shared() in one coroutine and
90 /// subsequently call unlock_shared() in another coroutine. In particular, it
91 /// is allowed to pass std::shared_lock<engine::Semaphore> across coroutines.
93
94 [[nodiscard]] bool try_lock_shared();
95
96 template <typename Rep, typename Period>
97 [[nodiscard]] bool try_lock_shared_for(std::chrono::duration<Rep, Period>);
98
99 template <typename Clock, typename Duration>
100 [[nodiscard]] bool try_lock_shared_until(
101 std::chrono::time_point<Clock, Duration>);
102
103 [[nodiscard]] bool try_lock_shared_until(Deadline deadline);
104
105 void lock_shared_count(Counter count);
106
107 void unlock_shared_count(Counter count);
108
109 [[nodiscard]] bool try_lock_shared_count(Counter count);
110
111 [[nodiscard]] bool try_lock_shared_until_count(Deadline deadline,
112 Counter count);
113
114 private:
115 enum class TryLockStatus { kSuccess, kTransientFailure, kPermanentFailure };
116 class SemaphoreWaitStrategy;
117
118 TryLockStatus DoTryLock(Counter count);
119 TryLockStatus LockFastPath(Counter count);
120 bool LockSlowPath(Deadline, Counter count);
121
122 impl::FastPimplWaitList lock_waiters_;
123 std::atomic<Counter> acquired_locks_;
124 std::atomic<Counter> capacity_;
125};
126
127/// @ingroup userver_concurrency
128///
129/// @brief Class that allows up to `max_simultaneous_locks` concurrent accesses
130/// to the critical section. It ignores task cancellation, unlike
131/// CancellableSemaphore.
132///
133/// ## Example usage:
134///
135/// @snippet engine/semaphore_test.cpp Sample engine::Semaphore usage
136///
137/// @see @ref scripts/docs/en/userver/synchronization.md
138class Semaphore final {
139 public:
140 using Counter = std::size_t;
141
142 /// Creates a semaphore with predefined number of available locks
143 /// @param capacity initial number of available locks
144 explicit Semaphore(Counter capacity);
145
146 ~Semaphore();
147
148 Semaphore(Semaphore&&) = delete;
149 Semaphore(const Semaphore&) = delete;
150 Semaphore& operator=(Semaphore&&) = delete;
151 Semaphore& operator=(const Semaphore&) = delete;
152
153 /// Sets the total number of available locks. If the lock count decreases, the
154 /// current acquired lock count may temporarily go above the limit.
155 void SetCapacity(Counter capacity);
156
157 /// Gets the total number of available locks.
158 [[nodiscard]] Counter GetCapacity() const noexcept;
159
160 /// Returns an approximate number of available locks, use only for statistics.
161 [[nodiscard]] std::size_t RemainingApprox() const;
162
163 /// Returns an approximate number of used locks, use only for statistics.
164 [[nodiscard]] std::size_t UsedApprox() const;
165
166 /// Decrements internal semaphore lock counter. Blocks if current counter is
167 /// zero until the subsequent call to unlock_shared() by another coroutine.
168 /// @note the user must eventually call unlock_shared() to increment the lock
169 /// counter.
170 /// @note the method waits for the semaphore even if the current task is
171 /// cancelled.
172 /// @throws UnreachableSemaphoreLockError if `capacity == 0`
174
175 /// Increments internal semaphore lock counter. If there is a user waiting in
176 /// lock_shared() on the same semaphore, it is woken up.
177 /// @note the order of coroutines to unblock is unspecified. Any code assuming
178 /// any specific order (e.g. FIFO) is incorrect and must be fixed.
179 /// @note it is allowed to call lock_shared() in one coroutine and
180 /// subsequently call unlock_shared() in another coroutine. In particular, it
181 /// is allowed to pass std::shared_lock<engine::Semaphore> across coroutines.
183
184 /// Decrements internal semaphore lock counter if current counter is
185 /// not zero.
186 /// @note unlock_shared() should be called later to increment the lock
187 /// counter.
188 [[nodiscard]] bool try_lock_shared();
189
190 template <typename Rep, typename Period>
191 [[nodiscard]] bool try_lock_shared_for(std::chrono::duration<Rep, Period>);
192
193 template <typename Clock, typename Duration>
194 [[nodiscard]] bool try_lock_shared_until(
195 std::chrono::time_point<Clock, Duration>);
196
197 [[nodiscard]] bool try_lock_shared_until(Deadline deadline);
198
199 void lock_shared_count(Counter count);
200
201 void unlock_shared_count(Counter count);
202
203 [[nodiscard]] bool try_lock_shared_count(Counter count);
204
205 [[nodiscard]] bool try_lock_shared_until_count(Deadline deadline,
206 Counter count);
207
208 private:
209 CancellableSemaphore sem_;
210};
211
212/// A replacement for std::shared_lock that accepts Deadline arguments
213class SemaphoreLock final {
214 public:
215 SemaphoreLock() noexcept = default;
216 explicit SemaphoreLock(Semaphore&);
217 SemaphoreLock(Semaphore&, std::defer_lock_t) noexcept;
218 SemaphoreLock(Semaphore&, std::try_to_lock_t);
219 SemaphoreLock(Semaphore&, std::adopt_lock_t) noexcept;
220
221 template <typename Rep, typename Period>
222 SemaphoreLock(Semaphore&, std::chrono::duration<Rep, Period>);
223
224 template <typename Clock, typename Duration>
225 SemaphoreLock(Semaphore&, std::chrono::time_point<Clock, Duration>);
226
227 SemaphoreLock(Semaphore&, Deadline);
228
229 ~SemaphoreLock();
230
231 SemaphoreLock(const SemaphoreLock&) = delete;
232 SemaphoreLock(SemaphoreLock&&) noexcept;
233 SemaphoreLock& operator=(const SemaphoreLock&) = delete;
234 SemaphoreLock& operator=(SemaphoreLock&&) noexcept;
235
236 [[nodiscard]] bool OwnsLock() const noexcept;
237 explicit operator bool() const noexcept { return OwnsLock(); }
238
239 void Lock();
240 bool TryLock();
241
242 template <typename Rep, typename Period>
243 bool TryLockFor(std::chrono::duration<Rep, Period>);
244
245 template <typename Clock, typename Duration>
246 bool TryLockUntil(std::chrono::time_point<Clock, Duration>);
247
248 bool TryLockUntil(Deadline);
249
250 void Unlock();
251 void Release();
252
253 private:
254 Semaphore* sem_{nullptr};
255 bool owns_lock_{false};
256};
257
258template <typename Rep, typename Period>
259bool Semaphore::try_lock_shared_for(
260 std::chrono::duration<Rep, Period> duration) {
261 return try_lock_shared_until(Deadline::FromDuration(duration));
262}
263
264template <typename Clock, typename Duration>
265bool Semaphore::try_lock_shared_until(
266 std::chrono::time_point<Clock, Duration> until) {
267 return try_lock_shared_until(Deadline::FromTimePoint(until));
268}
269
270template <typename Rep, typename Period>
271bool CancellableSemaphore::try_lock_shared_for(
272 std::chrono::duration<Rep, Period> duration) {
273 return try_lock_shared_until(Deadline::FromDuration(duration));
274}
275
276template <typename Clock, typename Duration>
277bool CancellableSemaphore::try_lock_shared_until(
278 std::chrono::time_point<Clock, Duration> until) {
279 return try_lock_shared_until(Deadline::FromTimePoint(until));
280}
281
282template <typename Rep, typename Period>
283SemaphoreLock::SemaphoreLock(Semaphore& sem,
284 std::chrono::duration<Rep, Period> duration)
285 : sem_(&sem) {
286 TryLockFor(duration);
287}
288
289template <typename Clock, typename Duration>
290SemaphoreLock::SemaphoreLock(Semaphore& sem,
291 std::chrono::time_point<Clock, Duration> until)
292 : sem_(&sem) {
293 TryLockUntil(until);
294}
295
296template <typename Rep, typename Period>
297bool SemaphoreLock::TryLockFor(std::chrono::duration<Rep, Period> duration) {
298 return TryLockUntil(Deadline::FromDuration(duration));
299}
300
301template <typename Clock, typename Duration>
302bool SemaphoreLock::TryLockUntil(
303 std::chrono::time_point<Clock, Duration> until) {
304 return TryLockUntil(Deadline::FromTimePoint(until));
305}
306
307} // namespace engine
308
309USERVER_NAMESPACE_END