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