@@ -3,6 +3,7 @@ import Foundation
3
3
#else
4
4
@preconcurrency import Foundation
5
5
#endif
6
+
6
7
import OrderedCollections
7
8
8
9
/// An event object that controls access to a resource between high and low priority tasks
@@ -15,20 +16,42 @@ import OrderedCollections
15
16
/// You can indicate high priority usage of resource by using ``increment(by:)`` method,
16
17
/// and indicate free of resource by calling ``signal(repeat:)`` or ``signal()`` methods.
17
18
/// 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
+ /// ```
19
36
///
20
37
/// Use the ``limit`` parameter to indicate concurrent low priority usage, i.e. if limit set to zero,
21
38
/// only one low priority usage allowed at one time.
22
- public actor AsyncCountdownEvent : AsyncObject {
39
+ public actor AsyncCountdownEvent : AsyncObject , ContinuableCollection {
23
40
/// The suspended tasks continuation type.
24
41
@usableFromInline
25
- typealias Continuation = SafeContinuation < GlobalContinuation < Void , Error > >
42
+ internal typealias Continuation = SafeContinuation <
43
+ GlobalContinuation < Void , Error >
44
+ >
26
45
/// The platform dependent lock used to synchronize continuations tracking.
27
46
@usableFromInline
28
- let locker : Locker = . init( )
47
+ internal let locker : Locker = . init( )
29
48
/// The continuations stored with an associated key for all the suspended task that are waiting to be resumed.
30
49
@usableFromInline
31
- private( set) var continuations : OrderedDictionary < UUID , Continuation > = [ : ]
50
+ internal private( set) var continuations :
51
+ OrderedDictionary <
52
+ UUID ,
53
+ Continuation
54
+ > = [ : ]
32
55
/// The limit up to which the countdown counts and triggers event.
33
56
///
34
57
/// By default this is set to zero and can be changed during initialization.
@@ -42,7 +65,7 @@ public actor AsyncCountdownEvent: AsyncObject {
42
65
///
43
66
/// Can be changed after initialization
44
67
/// by using ``reset(to:)`` method.
45
- public private ( set ) var initialCount : UInt
68
+ public var initialCount : UInt
46
69
/// Indicates whether countdown event current count is within ``limit``.
47
70
///
48
71
/// Queued tasks are resumed from suspension when event is set and until current count exceeds limit.
@@ -54,13 +77,13 @@ public actor AsyncCountdownEvent: AsyncObject {
54
77
///
55
78
/// - Returns: Whether to wait to be resumed later.
56
79
@inlinable
57
- func _wait ( ) -> Bool { !isSet || !continuations. isEmpty }
80
+ internal func shouldWait ( ) -> Bool { !isSet || !continuations. isEmpty }
58
81
59
82
/// Resume provided continuation with additional changes based on the associated flags.
60
83
///
61
84
/// - Parameter continuation: The queued continuation to resume.
62
85
@inlinable
63
- func _resumeContinuation ( _ continuation: Continuation ) {
86
+ internal func resumeContinuation ( _ continuation: Continuation ) {
64
87
currentCount += 1
65
88
continuation. resume ( )
66
89
}
@@ -71,12 +94,12 @@ public actor AsyncCountdownEvent: AsyncObject {
71
94
/// - continuation: The `continuation` to add.
72
95
/// - key: The key in the map.
73
96
@inlinable
74
- func _addContinuation (
97
+ internal func addContinuation (
75
98
_ continuation: Continuation ,
76
99
withKey key: UUID
77
100
) {
78
101
guard !continuation. resumed else { return }
79
- guard _wait ( ) else { _resumeContinuation ( continuation) ; return }
102
+ guard shouldWait ( ) else { resumeContinuation ( continuation) ; return }
80
103
continuations [ key] = continuation
81
104
}
82
105
@@ -85,49 +108,52 @@ public actor AsyncCountdownEvent: AsyncObject {
85
108
///
86
109
/// - Parameter key: The key in the map.
87
110
@inlinable
88
- func _removeContinuation ( withKey key: UUID ) {
111
+ internal func removeContinuation ( withKey key: UUID ) {
89
112
continuations. removeValue ( forKey: key)
90
113
}
91
114
92
115
/// Decrements countdown count by the provided number.
93
116
///
94
117
/// - Parameter number: The number to decrement count by.
95
118
@inlinable
96
- func _decrementCount ( by number: UInt = 1 ) {
97
- defer { _resumeContinuations ( ) }
119
+ internal func decrementCount ( by number: UInt = 1 ) {
120
+ defer { resumeContinuations ( ) }
98
121
guard currentCount > 0 else { return }
99
122
currentCount -= number
100
123
}
101
124
102
125
/// Resume previously waiting continuations for countdown event.
103
126
@inlinable
104
- func _resumeContinuations ( ) {
127
+ internal func resumeContinuations ( ) {
105
128
while !continuations. isEmpty && isSet {
106
129
let ( _, continuation) = continuations. removeFirst ( )
107
- _resumeContinuation ( continuation)
130
+ resumeContinuation ( continuation)
108
131
}
109
132
}
110
133
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.
113
135
///
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.
117
150
///
118
- /// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error .
151
+ /// - Parameter count: The new initial count .
119
152
@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 ( )
131
157
}
132
158
133
159
// MARK: Public
@@ -158,47 +184,97 @@ public actor AsyncCountdownEvent: AsyncObject {
158
184
/// Use this to indicate usage of resource from high priority tasks.
159
185
///
160
186
/// - 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) }
163
194
}
164
195
165
196
/// Resets current count to initial count.
166
197
///
167
198
/// If the current count becomes less or equal to limit, multiple queued tasks
168
199
/// 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 ( ) }
172
214
}
173
215
174
216
/// Resets initial count and current count to specified value.
175
217
///
176
218
/// If the current count becomes less or equal to limit, multiple queued tasks
177
219
/// are resumed from suspension until current count exceeds limit.
178
220
///
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) }
184
236
}
185
237
186
238
/// Registers a signal (decrements) with the countdown event.
187
239
///
188
240
/// Decrement the countdown. If the current count becomes less or equal to limit,
189
241
/// 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 ) }
192
256
}
193
257
194
258
/// Registers multiple signals (decrements by provided count) with the countdown event.
195
259
///
196
260
/// Decrement the countdown by the provided count. If the current count becomes less or equal to limit,
197
261
/// multiple queued tasks are resumed from suspension until current count exceeds limit.
198
262
///
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) }
202
278
}
203
279
204
280
/// Waits for, or increments, a countdown event.
@@ -207,9 +283,23 @@ public actor AsyncCountdownEvent: AsyncObject {
207
283
/// Otherwise, current task is suspended until either a signal occurs or event is reset.
208
284
///
209
285
/// 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.
210
296
@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 ( )
214
304
}
215
305
}
0 commit comments