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 {
21public:
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 {
27public:
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 {
42public:
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(std::chrono::time_point<Clock, Duration>);
101
102 [[nodiscard]] bool try_lock_shared_until(Deadline deadline);
103
104 void lock_shared_count(Counter count);
105
106 void unlock_shared_count(Counter count);
107
108 [[nodiscard]] bool try_lock_shared_count(Counter count);
109
110 [[nodiscard]] bool try_lock_shared_until_count(Deadline deadline, Counter count);
111
112private:
113 enum class TryLockStatus { kSuccess, kTransientFailure, kPermanentFailure };
114 class SemaphoreWaitStrategy;
115
116 TryLockStatus DoTryLock(Counter count);
117 TryLockStatus LockFastPath(Counter count);
118 bool LockSlowPath(Deadline, Counter count);
119
120 impl::FastPimplWaitList lock_waiters_;
121 std::atomic<Counter> acquired_locks_;
122 std::atomic<Counter> capacity_;
123};
124
125/// @ingroup userver_concurrency
126///
127/// @brief Class that allows up to `max_simultaneous_locks` concurrent accesses
128/// to the critical section. It ignores task cancellation, unlike
129/// CancellableSemaphore.
130///
131/// ## Example usage:
132///
133/// @snippet engine/semaphore_test.cpp Sample engine::Semaphore usage
134///
135/// @see @ref scripts/docs/en/userver/synchronization.md
136class Semaphore final {
137public:
138 using Counter = std::size_t;
139
140 /// Creates a semaphore with predefined number of available locks
141 /// @param capacity initial number of available locks
142 explicit Semaphore(Counter capacity);
143
144 ~Semaphore();
145
146 Semaphore(Semaphore&&) = delete;
147 Semaphore(const Semaphore&) = delete;
148 Semaphore& operator=(Semaphore&&) = delete;
149 Semaphore& operator=(const Semaphore&) = delete;
150
151 /// Sets the total number of available locks. If the lock count decreases, the
152 /// current acquired lock count may temporarily go above the limit.
153 void SetCapacity(Counter capacity);
154
155 /// Gets the total number of available locks.
156 [[nodiscard]] Counter GetCapacity() const noexcept;
157
158 /// Returns an approximate number of available locks, use only for statistics.
159 [[nodiscard]] std::size_t RemainingApprox() const;
160
161 /// Returns an approximate number of used locks, use only for statistics.
162 [[nodiscard]] std::size_t UsedApprox() const;
163
164 /// Decrements internal semaphore lock counter. Blocks if current counter is
165 /// zero until the subsequent call to unlock_shared() by another coroutine.
166 /// @note the user must eventually call unlock_shared() to increment the lock
167 /// counter.
168 /// @note the method waits for the semaphore even if the current task is
169 /// cancelled.
170 /// @throws UnreachableSemaphoreLockError if `capacity == 0`
172
173 /// Increments internal semaphore lock counter. If there is a user waiting in
174 /// lock_shared() on the same semaphore, it is woken up.
175 /// @note the order of coroutines to unblock is unspecified. Any code assuming
176 /// any specific order (e.g. FIFO) is incorrect and must be fixed.
177 /// @note it is allowed to call lock_shared() in one coroutine and
178 /// subsequently call unlock_shared() in another coroutine. In particular, it
179 /// is allowed to pass std::shared_lock<engine::Semaphore> across coroutines.
181
182 /// Decrements internal semaphore lock counter if current counter is
183 /// not zero.
184 /// @note unlock_shared() should be called later to increment the lock
185 /// counter.
186 [[nodiscard]] bool try_lock_shared();
187
188 template <typename Rep, typename Period>
189 [[nodiscard]] bool try_lock_shared_for(std::chrono::duration<Rep, Period>);
190
191 template <typename Clock, typename Duration>
192 [[nodiscard]] bool try_lock_shared_until(std::chrono::time_point<Clock, Duration>);
193
194 [[nodiscard]] bool try_lock_shared_until(Deadline deadline);
195
196 void lock_shared_count(Counter count);
197
198 void unlock_shared_count(Counter count);
199
200 [[nodiscard]] bool try_lock_shared_count(Counter count);
201
202 [[nodiscard]] bool try_lock_shared_until_count(Deadline deadline, Counter count);
203
204private:
205 CancellableSemaphore sem_;
206};
207
208/// A replacement for std::shared_lock that accepts Deadline arguments
209class SemaphoreLock final {
210public:
211 SemaphoreLock() noexcept = default;
212 explicit SemaphoreLock(Semaphore&);
213 SemaphoreLock(Semaphore&, std::defer_lock_t) noexcept;
214 SemaphoreLock(Semaphore&, std::try_to_lock_t);
215 SemaphoreLock(Semaphore&, std::adopt_lock_t) noexcept;
216
217 template <typename Rep, typename Period>
218 SemaphoreLock(Semaphore&, std::chrono::duration<Rep, Period>);
219
220 template <typename Clock, typename Duration>
221 SemaphoreLock(Semaphore&, std::chrono::time_point<Clock, Duration>);
222
223 SemaphoreLock(Semaphore&, Deadline);
224
225 ~SemaphoreLock();
226
227 SemaphoreLock(const SemaphoreLock&) = delete;
228 SemaphoreLock(SemaphoreLock&&) noexcept;
229 SemaphoreLock& operator=(const SemaphoreLock&) = delete;
230 SemaphoreLock& operator=(SemaphoreLock&&) noexcept;
231
232 [[nodiscard]] bool OwnsLock() const noexcept;
233 explicit operator bool() const noexcept { return OwnsLock(); }
234
235 void Lock();
236 bool TryLock();
237
238 template <typename Rep, typename Period>
239 bool TryLockFor(std::chrono::duration<Rep, Period>);
240
241 template <typename Clock, typename Duration>
242 bool TryLockUntil(std::chrono::time_point<Clock, Duration>);
243
244 bool TryLockUntil(Deadline);
245
246 void Unlock();
247 void Release();
248
249private:
250 Semaphore* sem_{nullptr};
251 bool owns_lock_{false};
252};
253
254template <typename Rep, typename Period>
255bool Semaphore::try_lock_shared_for(std::chrono::duration<Rep, Period> duration) {
256 return try_lock_shared_until(Deadline::FromDuration(duration));
257}
258
259template <typename Clock, typename Duration>
260bool Semaphore::try_lock_shared_until(std::chrono::time_point<Clock, Duration> until) {
261 return try_lock_shared_until(Deadline::FromTimePoint(until));
262}
263
264template <typename Rep, typename Period>
265bool CancellableSemaphore::try_lock_shared_for(std::chrono::duration<Rep, Period> duration) {
266 return try_lock_shared_until(Deadline::FromDuration(duration));
267}
268
269template <typename Clock, typename Duration>
270bool CancellableSemaphore::try_lock_shared_until(std::chrono::time_point<Clock, Duration> until) {
271 return try_lock_shared_until(Deadline::FromTimePoint(until));
272}
273
274template <typename Rep, typename Period>
275SemaphoreLock::SemaphoreLock(Semaphore& sem, std::chrono::duration<Rep, Period> duration) : sem_(&sem) {
276 TryLockFor(duration);
277}
278
279template <typename Clock, typename Duration>
280SemaphoreLock::SemaphoreLock(Semaphore& sem, std::chrono::time_point<Clock, Duration> until) : sem_(&sem) {
281 TryLockUntil(until);
282}
283
284template <typename Rep, typename Period>
285bool SemaphoreLock::TryLockFor(std::chrono::duration<Rep, Period> duration) {
286 return TryLockUntil(Deadline::FromDuration(duration));
287}
288
289template <typename Clock, typename Duration>
290bool SemaphoreLock::TryLockUntil(std::chrono::time_point<Clock, Duration> until) {
291 return TryLockUntil(Deadline::FromTimePoint(until));
292}
293
294} // namespace engine
295
296USERVER_NAMESPACE_END