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
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