@@ -3,6 +3,7 @@ import Foundation
33#else
44@preconcurrency import Foundation
55#endif
6+
67import OrderedCollections
78
89/// An event object that controls access to a resource between high and low priority tasks
@@ -15,20 +16,42 @@ import OrderedCollections
1516/// You can indicate high priority usage of resource by using ``increment(by:)`` method,
1617/// and indicate free of resource by calling ``signal(repeat:)`` or ``signal()`` methods.
1718/// For low priority resource usage or detect resource idling use ``wait()`` method
18- /// or its timeout variation ``wait(forNanoseconds:)``.
19+ /// or its timeout variation ``wait(forNanoseconds:)``:
20+ ///
21+ /// ```swift
22+ /// // create event with initial count and count down limit
23+ /// let event = AsyncCountdownEvent()
24+ /// // increment countdown count from high priority tasks
25+ /// event.increment(by: 1)
26+ ///
27+ /// // wait for countdown signal from low priority tasks,
28+ /// // fails only if task cancelled
29+ /// try await event.wait()
30+ /// // or wait with some timeout
31+ /// try await event.wait(forNanoseconds: 1_000_000_000)
32+ ///
33+ /// // signal countdown after completing high priority tasks
34+ /// event.signal()
35+ /// ```
1936///
2037/// Use the ``limit`` parameter to indicate concurrent low priority usage, i.e. if limit set to zero,
2138/// only one low priority usage allowed at one time.
22- public actor AsyncCountdownEvent : AsyncObject {
39+ public actor AsyncCountdownEvent : AsyncObject , ContinuableCollection {
2340 /// The suspended tasks continuation type.
2441 @usableFromInline
25- typealias Continuation = SafeContinuation < GlobalContinuation < Void , Error > >
42+ internal typealias Continuation = SafeContinuation <
43+ GlobalContinuation < Void , Error >
44+ >
2645 /// The platform dependent lock used to synchronize continuations tracking.
2746 @usableFromInline
28- let locker : Locker = . init( )
47+ internal let locker : Locker = . init( )
2948 /// The continuations stored with an associated key for all the suspended task that are waiting to be resumed.
3049 @usableFromInline
31- private( set) var continuations : OrderedDictionary < UUID , Continuation > = [ : ]
50+ internal private( set) var continuations :
51+ OrderedDictionary <
52+ UUID ,
53+ Continuation
54+ > = [ : ]
3255 /// The limit up to which the countdown counts and triggers event.
3356 ///
3457 /// By default this is set to zero and can be changed during initialization.
@@ -42,7 +65,7 @@ public actor AsyncCountdownEvent: AsyncObject {
4265 ///
4366 /// Can be changed after initialization
4467 /// by using ``reset(to:)`` method.
45- public private ( set ) var initialCount : UInt
68+ public var initialCount : UInt
4669 /// Indicates whether countdown event current count is within ``limit``.
4770 ///
4871 /// Queued tasks are resumed from suspension when event is set and until current count exceeds limit.
@@ -54,13 +77,13 @@ public actor AsyncCountdownEvent: AsyncObject {
5477 ///
5578 /// - Returns: Whether to wait to be resumed later.
5679 @inlinable
57- func _wait ( ) -> Bool { !isSet || !continuations. isEmpty }
80+ internal func shouldWait ( ) -> Bool { !isSet || !continuations. isEmpty }
5881
5982 /// Resume provided continuation with additional changes based on the associated flags.
6083 ///
6184 /// - Parameter continuation: The queued continuation to resume.
6285 @inlinable
63- func _resumeContinuation ( _ continuation: Continuation ) {
86+ internal func resumeContinuation ( _ continuation: Continuation ) {
6487 currentCount += 1
6588 continuation. resume ( )
6689 }
@@ -71,12 +94,12 @@ public actor AsyncCountdownEvent: AsyncObject {
7194 /// - continuation: The `continuation` to add.
7295 /// - key: The key in the map.
7396 @inlinable
74- func _addContinuation (
97+ internal func addContinuation (
7598 _ continuation: Continuation ,
7699 withKey key: UUID
77100 ) {
78101 guard !continuation. resumed else { return }
79- guard _wait ( ) else { _resumeContinuation ( continuation) ; return }
102+ guard shouldWait ( ) else { resumeContinuation ( continuation) ; return }
80103 continuations [ key] = continuation
81104 }
82105
@@ -85,49 +108,52 @@ public actor AsyncCountdownEvent: AsyncObject {
85108 ///
86109 /// - Parameter key: The key in the map.
87110 @inlinable
88- func _removeContinuation ( withKey key: UUID ) {
111+ internal func removeContinuation ( withKey key: UUID ) {
89112 continuations. removeValue ( forKey: key)
90113 }
91114
92115 /// Decrements countdown count by the provided number.
93116 ///
94117 /// - Parameter number: The number to decrement count by.
95118 @inlinable
96- func _decrementCount ( by number: UInt = 1 ) {
97- defer { _resumeContinuations ( ) }
119+ internal func decrementCount ( by number: UInt = 1 ) {
120+ defer { resumeContinuations ( ) }
98121 guard currentCount > 0 else { return }
99122 currentCount -= number
100123 }
101124
102125 /// Resume previously waiting continuations for countdown event.
103126 @inlinable
104- func _resumeContinuations ( ) {
127+ internal func resumeContinuations ( ) {
105128 while !continuations. isEmpty && isSet {
106129 let ( _, continuation) = continuations. removeFirst ( )
107- _resumeContinuation ( continuation)
130+ resumeContinuation ( continuation)
108131 }
109132 }
110133
111- /// Suspends the current task, then calls the given closure with a throwing continuation for the current task.
112- /// Continuation can be cancelled with error if current task is cancelled, by invoking `_removeContinuation`.
134+ /// Increments the countdown event current count by the specified value.
113135 ///
114- /// Spins up a new continuation and requests to track it with key by invoking `_addContinuation`.
115- /// This operation cooperatively checks for cancellation and reacting to it by invoking `_removeContinuation`.
116- /// Continuation can be resumed with error and some cleanup code can be run here.
136+ /// - Parameter count: The value by which to increase ``currentCount``.
137+ @inlinable
138+ internal func incrementCount( by count: UInt = 1 ) {
139+ self . currentCount += count
140+ }
141+
142+ /// Resets current count to initial count.
143+ @inlinable
144+ internal func resetCount( ) {
145+ self . currentCount = initialCount
146+ resumeContinuations ( )
147+ }
148+
149+ /// Resets initial count and current count to specified value.
117150 ///
118- /// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error .
151+ /// - Parameter count: The new initial count .
119152 @inlinable
120- nonisolated func _withPromisedContinuation( ) async throws {
121- let key = UUID ( )
122- try await Continuation . withCancellation ( synchronizedWith: locker) {
123- Task { [ weak self] in
124- await self ? . _removeContinuation ( withKey: key)
125- }
126- } operation: { continuation in
127- Task { [ weak self] in
128- await self ? . _addContinuation ( continuation, withKey: key)
129- }
130- }
153+ internal func resetCount( to count: UInt ) {
154+ initialCount = count
155+ self . currentCount = count
156+ resumeContinuations ( )
131157 }
132158
133159 // MARK: Public
@@ -158,47 +184,97 @@ public actor AsyncCountdownEvent: AsyncObject {
158184 /// Use this to indicate usage of resource from high priority tasks.
159185 ///
160186 /// - Parameter count: The value by which to increase ``currentCount``.
161- public func increment( by count: UInt = 1 ) {
162- self . currentCount += count
187+ public nonisolated func increment(
188+ by count: UInt = 1 ,
189+ file: String = #fileID,
190+ function: String = #function,
191+ line: UInt = #line
192+ ) {
193+ Task { await incrementCount ( by: count) }
163194 }
164195
165196 /// Resets current count to initial count.
166197 ///
167198 /// If the current count becomes less or equal to limit, multiple queued tasks
168199 /// are resumed from suspension until current count exceeds limit.
169- public func reset( ) {
170- self . currentCount = initialCount
171- _resumeContinuations ( )
200+ ///
201+ /// - Parameters:
202+ /// - file: The file reset originates from (there's usually no need to pass it
203+ /// explicitly as it defaults to `#fileID`).
204+ /// - function: The function reset originates from (there's usually no need to
205+ /// pass it explicitly as it defaults to `#function`).
206+ /// - line: The line reset originates from (there's usually no need to pass it
207+ /// explicitly as it defaults to `#line`).
208+ public nonisolated func reset(
209+ file: String = #fileID,
210+ function: String = #function,
211+ line: UInt = #line
212+ ) {
213+ Task { await resetCount ( ) }
172214 }
173215
174216 /// Resets initial count and current count to specified value.
175217 ///
176218 /// If the current count becomes less or equal to limit, multiple queued tasks
177219 /// are resumed from suspension until current count exceeds limit.
178220 ///
179- /// - Parameter count: The new initial count.
180- public func reset( to count: UInt ) {
181- initialCount = count
182- self . currentCount = count
183- _resumeContinuations ( )
221+ /// - Parameters:
222+ /// - count: The new initial count.
223+ /// - file: The file reset originates from (there's usually no need to pass it
224+ /// explicitly as it defaults to `#fileID`).
225+ /// - function: The function reset originates from (there's usually no need to
226+ /// pass it explicitly as it defaults to `#function`).
227+ /// - line: The line reset originates from (there's usually no need to pass it
228+ /// explicitly as it defaults to `#line`).
229+ public nonisolated func reset(
230+ to count: UInt ,
231+ file: String = #fileID,
232+ function: String = #function,
233+ line: UInt = #line
234+ ) {
235+ Task { await resetCount ( to: count) }
184236 }
185237
186238 /// Registers a signal (decrements) with the countdown event.
187239 ///
188240 /// Decrement the countdown. If the current count becomes less or equal to limit,
189241 /// one queued task is resumed from suspension.
190- public func signal( ) {
191- signal ( repeat : 1 )
242+ ///
243+ /// - Parameters:
244+ /// - file: The file signal originates from (there's usually no need to pass it
245+ /// explicitly as it defaults to `#fileID`).
246+ /// - function: The function signal originates from (there's usually no need to
247+ /// pass it explicitly as it defaults to `#function`).
248+ /// - line: The line signal originates from (there's usually no need to pass it
249+ /// explicitly as it defaults to `#line`).
250+ public nonisolated func signal(
251+ file: String = #fileID,
252+ function: String = #function,
253+ line: UInt = #line
254+ ) {
255+ Task { await decrementCount ( by: 1 ) }
192256 }
193257
194258 /// Registers multiple signals (decrements by provided count) with the countdown event.
195259 ///
196260 /// Decrement the countdown by the provided count. If the current count becomes less or equal to limit,
197261 /// multiple queued tasks are resumed from suspension until current count exceeds limit.
198262 ///
199- /// - Parameter count: The number of signals to register.
200- public func signal( repeat count: UInt ) {
201- _decrementCount ( by: count)
263+ /// - Parameters:
264+ /// - count: The number of signals to register.
265+ /// - file: The file signal originates from (there's usually no need to pass it
266+ /// explicitly as it defaults to `#fileID`).
267+ /// - function: The function signal originates from (there's usually no need to
268+ /// pass it explicitly as it defaults to `#function`).
269+ /// - line: The line signal originates from (there's usually no need to pass it
270+ /// explicitly as it defaults to `#line`).
271+ public nonisolated func signal(
272+ repeat count: UInt ,
273+ file: String = #fileID,
274+ function: String = #function,
275+ line: UInt = #line
276+ ) {
277+ Task { await decrementCount ( by: count) }
202278 }
203279
204280 /// Waits for, or increments, a countdown event.
@@ -207,9 +283,23 @@ public actor AsyncCountdownEvent: AsyncObject {
207283 /// Otherwise, current task is suspended until either a signal occurs or event is reset.
208284 ///
209285 /// Use this to wait for high priority tasks completion to start low priority ones.
286+ ///
287+ /// - Parameters:
288+ /// - file: The file wait request originates from (there's usually no need to pass it
289+ /// explicitly as it defaults to `#fileID`).
290+ /// - function: The function wait request originates from (there's usually no need to
291+ /// pass it explicitly as it defaults to `#function`).
292+ /// - line: The line wait request originates from (there's usually no need to pass it
293+ /// explicitly as it defaults to `#line`).
294+ ///
295+ /// - Throws: `CancellationError` if cancelled.
210296 @Sendable
211- public func wait( ) async {
212- guard _wait ( ) else { currentCount += 1 ; return }
213- try ? await _withPromisedContinuation ( )
297+ public func wait(
298+ file: String = #fileID,
299+ function: String = #function,
300+ line: UInt = #line
301+ ) async throws {
302+ guard shouldWait ( ) else { currentCount += 1 ; return }
303+ try await withPromisedContinuation ( )
214304 }
215305}
0 commit comments