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 Counter GetCapacity() const noexcept;
66
67 /// Returns an approximate number of available locks, use only for statistics.
68 std::size_t RemainingApprox() const;
69
70 /// Returns an approximate number of used locks, use only for statistics.
71 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 /// @note the user must eventually call unlock_shared() to increment the lock
76 /// counter.
77 /// @note the method doesn't wait for the semaphore if the current task is
78 /// cancelled. If a task waits on CancellableSemaphore and the cancellation
79 /// is requested, the waiting is aborted with an exception.
80 /// @throws UnreachableSemaphoreLockError if `capacity == 0`
81 /// @throws SemaphoreLockCancelledError if the current task is cancelled
83
84 /// Increments internal semaphore lock counter. If there is a user waiting in
85 /// lock_shared() on the same semaphore, it will be waken up.
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 /// @note it is allowed to call lock_shared() in one coroutine and
89 /// subsequently call unlock_shared() in another coroutine. In particular, it
90 /// is allowed to pass std::shared_lock<engine::Semaphore> across coroutines.
92
93 [[nodiscard]] bool try_lock_shared();
94
95 template <typename Rep, typename Period>
96 [[nodiscard]] bool try_lock_shared_for(std::chrono::duration<Rep, Period>);
97
98 template <typename Clock, typename Duration>
99 [[nodiscard]] bool try_lock_shared_until(
100 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,
111 Counter count);
112
113 private:
114 enum class TryLockStatus { kSuccess, kTransientFailure, kPermanentFailure };
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 {
137 public:
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 Counter GetCapacity() const noexcept;
157
158 /// Returns an approximate number of available locks, use only for statistics.
159 std::size_t RemainingApprox() const;
160
161 /// Returns an approximate number of used locks, use only for statistics.
162 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 will be waken 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 [[nodiscard]] bool try_lock_shared();
183
184 template <typename Rep, typename Period>
185 [[nodiscard]] bool try_lock_shared_for(std::chrono::duration<Rep, Period>);
186
187 template <typename Clock, typename Duration>
188 [[nodiscard]] bool try_lock_shared_until(
189 std::chrono::time_point<Clock, Duration>);
190
191 [[nodiscard]] bool try_lock_shared_until(Deadline deadline);
192
193 void lock_shared_count(Counter count);
194
195 void unlock_shared_count(Counter count);
196
197 [[nodiscard]] bool try_lock_shared_count(Counter count);
198
199 [[nodiscard]] bool try_lock_shared_until_count(Deadline deadline,
200 Counter count);
201
202 private:
203 CancellableSemaphore sem_;
204};
205
206/// A replacement for std::shared_lock that accepts Deadline arguments
207class SemaphoreLock final {
208 public:
209 SemaphoreLock() noexcept = default;
210 explicit SemaphoreLock(Semaphore&);
211 SemaphoreLock(Semaphore&, std::defer_lock_t) noexcept;
212 SemaphoreLock(Semaphore&, std::try_to_lock_t);
213 SemaphoreLock(Semaphore&, std::adopt_lock_t) noexcept;
214
215 template <typename Rep, typename Period>
216 SemaphoreLock(Semaphore&, std::chrono::duration<Rep, Period>);
217
218 template <typename Clock, typename Duration>
219 SemaphoreLock(Semaphore&, std::chrono::time_point<Clock, Duration>);
220
221 SemaphoreLock(Semaphore&, Deadline);
222
223 ~SemaphoreLock();
224
225 SemaphoreLock(const SemaphoreLock&) = delete;
226 SemaphoreLock(SemaphoreLock&&) noexcept;
227 SemaphoreLock& operator=(const SemaphoreLock&) = delete;
228 SemaphoreLock& operator=(SemaphoreLock&&) noexcept;
229
230 bool OwnsLock() const noexcept;
231 explicit operator bool() const noexcept { return OwnsLock(); }
232
233 void Lock();
234 bool TryLock();
235
236 template <typename Rep, typename Period>
237 bool TryLockFor(std::chrono::duration<Rep, Period>);
238
239 template <typename Clock, typename Duration>
240 bool TryLockUntil(std::chrono::time_point<Clock, Duration>);
241
242 bool TryLockUntil(Deadline);
243
244 void Unlock();
245 void Release();
246
247 private:
248 Semaphore* sem_{nullptr};
249 bool owns_lock_{false};
250};
251
252template <typename Rep, typename Period>
253bool Semaphore::try_lock_shared_for(
254 std::chrono::duration<Rep, Period> duration) {
255 return try_lock_shared_until(Deadline::FromDuration(duration));
256}
257
258template <typename Clock, typename Duration>
259bool Semaphore::try_lock_shared_until(
260 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(
266 std::chrono::duration<Rep, Period> duration) {
267 return try_lock_shared_until(Deadline::FromDuration(duration));
268}
269
270template <typename Clock, typename Duration>
271bool CancellableSemaphore::try_lock_shared_until(
272 std::chrono::time_point<Clock, Duration> until) {
273 return try_lock_shared_until(Deadline::FromTimePoint(until));
274}
275
276template <typename Rep, typename Period>
277SemaphoreLock::SemaphoreLock(Semaphore& sem,
278 std::chrono::duration<Rep, Period> duration)
279 : sem_(&sem) {
280 TryLockFor(duration);
281}
282
283template <typename Clock, typename Duration>
284SemaphoreLock::SemaphoreLock(Semaphore& sem,
285 std::chrono::time_point<Clock, Duration> until)
286 : sem_(&sem) {
287 TryLockUntil(until);
288}
289
290template <typename Rep, typename Period>
291bool SemaphoreLock::TryLockFor(std::chrono::duration<Rep, Period> duration) {
292 return TryLockUntil(Deadline::FromDuration(duration));
293}
294
295template <typename Clock, typename Duration>
296bool SemaphoreLock::TryLockUntil(
297 std::chrono::time_point<Clock, Duration> until) {
298 return TryLockUntil(Deadline::FromTimePoint(until));
299}
300
301} // namespace engine
302
303USERVER_NAMESPACE_END