Skip to content

Commit 68702b5

Browse files
committed
feat: add wait for multiple synchronization objects
1 parent ad42d72 commit 68702b5

File tree

5 files changed

+272
-99
lines changed

5 files changed

+272
-99
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Platforms](https://img.shields.io/badge/Platforms-all-sucess)](https://img.shields.io/badge/Platforms-all-sucess)
88
[![CI/CD](https://github.com/SwiftyLab/AsyncObjects/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/SwiftyLab/AsyncObjects/actions/workflows/main.yml)
99
[![CodeQL](https://github.com/SwiftyLab/AsyncObjects/actions/workflows/codeql-analysis.yml/badge.svg?event=schedule)](https://github.com/SwiftyLab/AsyncObjects/actions/workflows/codeql-analysis.yml)
10-
[![Maintainability](https://api.codeclimate.com/v1/badges/a489bc673af55864ff66/maintainability)](https://codeclimate.com/github/SwiftyLab/AsyncObjects/maintainability)
10+
[![Maintainability](https://api.codeclimate.com/v1/badges/37183c809818826c1bcf/maintainability)](https://codeclimate.com/github/SwiftyLab/AsyncObjects/maintainability)
1111
[![codecov](https://codecov.io/gh/SwiftyLab/AsyncObjects/branch/main/graph/badge.svg?token=jKxMv5oFeA)](https://codecov.io/gh/SwiftyLab/AsyncObjects)
1212
<!-- [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/AsyncObjects.svg?label=CocoaPods&color=C90005)](https://badge.fury.io/co/AsyncObjects) -->
1313

Sources/AsyncObjects/AsyncEvent.swift

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Foundation
66
///
77
/// You can signal event by calling the ``signal()`` method and reset signal by calling ``reset()``.
88
/// Wait for event signal by calling ``wait()`` method or its timeout variation ``wait(forNanoseconds:)``.
9-
public actor AsyncEvent {
9+
public actor AsyncEvent: AsyncObject {
1010
/// The suspended tasks continuation type.
1111
private typealias Continuation = UnsafeContinuation<Void, Error>
1212
/// The continuations stored with an associated key for all the suspended task that are waitig for event signal.
@@ -19,6 +19,7 @@ public actor AsyncEvent {
1919
/// - Parameters:
2020
/// - continuation: The `continuation` to add.
2121
/// - key: The key in the map.
22+
@inline(__always)
2223
private func addContinuation(
2324
_ continuation: Continuation,
2425
withKey key: UUID
@@ -30,6 +31,7 @@ public actor AsyncEvent {
3031
/// from `continuations` map.
3132
///
3233
/// - Parameter key: The key in the map.
34+
@inline(__always)
3335
private func removeContinuation(withKey key: UUID) {
3436
continuations.removeValue(forKey: key)
3537
}
@@ -43,16 +45,20 @@ public actor AsyncEvent {
4345
self.signaled = signaled
4446
}
4547

48+
deinit { self.continuations.forEach { $0.value.cancel() } }
49+
4650
/// Resets signal of event.
4751
///
4852
/// After reset, tasks have to wait for event signal to complete.
53+
@Sendable
4954
public func reset() {
5055
signaled = false
5156
}
5257

5358
/// Signals the event.
5459
///
5560
/// Resumes all the tasks suspended and waiting for signal.
61+
@Sendable
5662
public func signal() {
5763
continuations.forEach { $0.value.resume() }
5864
continuations = [:]
@@ -63,47 +69,20 @@ public actor AsyncEvent {
6369
///
6470
/// Only waits asynchronously, if event is in non-signaled state,
6571
/// until event is signaled.
72+
@Sendable
6673
public func wait() async {
6774
guard !signaled else { return }
6875
let key = UUID()
69-
do {
70-
try await withUnsafeThrowingContinuationCancellationHandler(
71-
handler: { (continuation: Continuation) in
72-
Task { await removeContinuation(withKey: key) }
73-
},
74-
{ addContinuation($0, withKey: key) }
75-
)
76-
} catch {
77-
debugPrint(
78-
"Wait on event for continuation task with key: \(key)"
79-
+ " cancelled with error \(error)"
80-
)
81-
}
82-
}
83-
84-
/// Waits for event signal within the duration, or proceeds if already signaled.
85-
///
86-
/// Only waits asynchronously, if event is in non-signaled state,
87-
/// until event is signaled or the provided timeout expires.
88-
///
89-
/// - Parameter duration: The duration in nano seconds to wait until.
90-
/// - Returns: The result indicating whether wait completed or timed out.
91-
public func wait(
92-
forNanoseconds duration: UInt64
93-
) async -> TaskTimeoutResult {
94-
guard !signaled else { return .success }
95-
await withTaskGroup(of: Void.self) { group in
96-
group.addTask { [weak self] in await self?.wait() }
97-
group.addTask {
98-
do {
99-
try await Task.sleep(nanoseconds: duration)
100-
} catch {}
101-
}
102-
103-
for await _ in group.prefix(1) {
104-
group.cancelAll()
76+
try? await withUnsafeThrowingContinuationCancellationHandler(
77+
handler: { [weak self] (continuation: Continuation) in
78+
Task { [weak self] in
79+
await self?.removeContinuation(withKey: key)
80+
}
81+
}, { [weak self] (continuation: Continuation) in
82+
Task { [weak self] in
83+
await self?.addContinuation(continuation, withKey: key)
84+
}
10585
}
106-
}
107-
return signaled ? .success : .timedOut
86+
)
10887
}
10988
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/// A result value indicating whether a task finished before a specified time.
2+
@frozen
3+
public enum TaskTimeoutResult: Hashable {
4+
/// Indicates that a task successfully finished
5+
/// before the specified time elapsed.
6+
case success
7+
/// Indicates that a task failed to finish
8+
/// before the specified time elapsed.
9+
case timedOut
10+
}
11+
12+
/// An object type that can provide synchonization accross multiple task contexts
13+
///
14+
/// Waiting asynchronously can be done by calling ``wait()`` method,
15+
/// while object decides when to resume task. Similarly, ``signal()`` can be used
16+
/// to indicate resuming of suspended tasks.
17+
public protocol AsyncObject: Sendable {
18+
/// Signals the object for task synchronization.
19+
///
20+
/// Object might resume suspended tasks
21+
/// or synchronize tasks differently.
22+
@Sendable
23+
func signal() async
24+
/// Waits for the object to green light task execution.
25+
///
26+
/// Waits asynchronously suspending current task, instead of blocking any thread.
27+
/// Async object has to resume the task at a later time depending on its requirement.
28+
///
29+
/// - Note: Method might return immediately depending upon the synchronization object requirement.
30+
@Sendable
31+
func wait() async
32+
/// Waits for the object to green light task execution within the duration.
33+
///
34+
/// Waits asynchronously suspending current task, instead of blocking any thread within the duration.
35+
/// Async object has to resume the task at a later time depending on its requirement.
36+
/// Depending upon whether wait succeeds or timeout expires result is returned.
37+
///
38+
/// - Parameter duration: The duration in nano seconds to wait until.
39+
/// - Returns: The result indicating whether wait completed or timed out.
40+
/// - Note: Method might return immediately depending upon the synchronization object requirement.
41+
@discardableResult
42+
@Sendable
43+
func wait(forNanoseconds duration: UInt64) async -> TaskTimeoutResult
44+
}
45+
46+
public extension AsyncObject where Self: AnyObject {
47+
/// Waits for the object to green light task execution within the duration.
48+
///
49+
/// Waits asynchronously suspending current task, instead of blocking any thread.
50+
/// Depending upon whether wait succeeds or timeout expires result is returned.
51+
/// Async object has to resume the task at a later time depending on its requirement.
52+
///
53+
/// - Parameter duration: The duration in nano seconds to wait until.
54+
/// - Returns: The result indicating whether wait completed or timed out.
55+
/// - Note: Method might return immediately depending upon the synchronization object requirement.
56+
@discardableResult
57+
@Sendable
58+
func wait(forNanoseconds duration: UInt64) async -> TaskTimeoutResult {
59+
return await waitForTaskCompletion(
60+
task: { [weak self] in await self?.wait() },
61+
withTimeoutInNanoseconds: duration
62+
)
63+
}
64+
}
65+
66+
/// Waits for multiple objects to green light task execution.
67+
///
68+
/// Invokes ``AsyncObject/wait()`` for all objects
69+
/// and returns only when all the invokation completes.
70+
///
71+
/// - Parameter objects: The objects to wait for.
72+
public func waitForAll(_ objects: [any AsyncObject]) async {
73+
await withTaskGroup(of: Void.self) { group in
74+
objects.forEach { group.addTask(operation: $0.wait) }
75+
await group.waitForAll()
76+
}
77+
}
78+
79+
/// Waits for multiple objects to green light task execution.
80+
///
81+
/// Invokes ``AsyncObject/wait()`` for all objects
82+
/// and returns only when all the invokation completes.
83+
///
84+
/// - Parameter objects: The objects to wait for.
85+
public func waitForAll(_ objects: any AsyncObject...) async {
86+
await waitForAll(objects)
87+
}
88+
89+
/// Waits for multiple objects to green light task execution
90+
/// within provided duration.
91+
///
92+
/// Invokes ``AsyncObject/wait()`` for all objects
93+
/// and returns either when all the invokation completes
94+
/// or the timeout expires.
95+
///
96+
/// - Parameters:
97+
/// - objects: The objects to wait for.
98+
/// - duration: The duration in nano seconds to wait until.
99+
/// - Returns: The result indicating whether wait completed or timed out.
100+
public func waitForAll(
101+
_ objects: [any AsyncObject],
102+
forNanoseconds duration: UInt64
103+
) async -> TaskTimeoutResult {
104+
return await waitForTaskCompletion(
105+
task: { await waitForAll(objects) },
106+
withTimeoutInNanoseconds: duration
107+
)
108+
}
109+
110+
/// Waits for multiple objects to green light task execution
111+
/// within provided duration.
112+
///
113+
/// Invokes ``AsyncObject/wait()`` for all objects
114+
/// and returns either when all the invokation completes
115+
/// or the timeout expires.
116+
///
117+
/// - Parameters:
118+
/// - objects: The objects to wait for.
119+
/// - duration: The duration in nano seconds to wait until.
120+
/// - Returns: The result indicating whether wait completed or timed out.
121+
public func waitForAll(
122+
_ objects: any AsyncObject...,
123+
forNanoseconds duration: UInt64
124+
) async -> TaskTimeoutResult {
125+
return await waitForAll(objects, forNanoseconds: duration)
126+
}
127+
128+
/// Waits for multiple objects to green light task execution
129+
/// by either of them.
130+
///
131+
/// Invokes ``AsyncObject/wait()`` for all objects
132+
/// and returns when any of the invokation completes.
133+
///
134+
/// - Parameter objects: The objects to wait for.
135+
public func waitForAny(_ objects: [any AsyncObject]) async {
136+
await withTaskGroup(of: Void.self) { group in
137+
objects.forEach { group.addTask(operation: $0.wait) }
138+
for await _ in group.prefix(1) { group.cancelAll() }
139+
}
140+
}
141+
142+
/// Waits for multiple objects to green light task execution
143+
/// by either of them.
144+
///
145+
/// Invokes ``AsyncObject/wait()`` for all objects
146+
/// and returns when any of the invokation completes.
147+
///
148+
/// - Parameter objects: The objects to wait for.
149+
public func waitForAny(_ objects: any AsyncObject...) async {
150+
await waitForAny(objects)
151+
}
152+
153+
/// Waits for multiple objects to green light task execution
154+
/// by either of them within provided duration.
155+
///
156+
/// Invokes ``AsyncObject/wait()`` for all objects
157+
/// and returns when any of the invokation completes
158+
/// or the timeout expires.
159+
///
160+
/// - Parameters:
161+
/// - objects: The objects to wait for.
162+
/// - duration: The duration in nano seconds to wait until.
163+
/// - Returns: The result indicating whether wait completed or timed out.
164+
public func waitForAny(
165+
_ objects: [any AsyncObject],
166+
forNanoseconds duration: UInt64
167+
) async -> TaskTimeoutResult {
168+
return await waitForTaskCompletion(
169+
task: { await waitForAny(objects) },
170+
withTimeoutInNanoseconds: duration
171+
)
172+
}
173+
174+
/// Waits for multiple objects to green light task execution
175+
/// by either of them within provided duration.
176+
///
177+
/// Invokes ``AsyncObject/wait()`` for all objects
178+
/// and returns when any of the invokation completes
179+
/// or the timeout expires.
180+
///
181+
/// - Parameters:
182+
/// - objects: The objects to wait for.
183+
/// - duration: The duration in nano seconds to wait until.
184+
/// - Returns: The result indicating whether wait completed or timed out.
185+
public func waitForAny(
186+
_ objects: any AsyncObject...,
187+
forNanoseconds duration: UInt64
188+
) async -> TaskTimeoutResult {
189+
return await waitForAny(objects, forNanoseconds: duration)
190+
}
191+
192+
/// Waits for the provided task to be completed within the timeout duration.
193+
///
194+
/// Executes the provided tasks and waits until timeout expires.
195+
/// If task doesn't complete within time frame, task is cancelled.
196+
///
197+
/// - Parameters:
198+
/// - task: The task to execute and wait for completion.
199+
/// - timeout: The duration in nano seconds to wait until.
200+
/// - Returns: The result indicating whether task execution completed
201+
/// or timed out.
202+
@inlinable
203+
public func waitForTaskCompletion(
204+
task: @escaping @Sendable () async -> Void,
205+
withTimeoutInNanoseconds timeout: UInt64
206+
) async -> TaskTimeoutResult {
207+
var timedOut = true
208+
await withTaskGroup(of: Bool.self) { group in
209+
group.addTask {
210+
await task()
211+
return !Task.isCancelled
212+
}
213+
group.addTask {
214+
(try? await Task.sleep(nanoseconds: timeout)) == nil
215+
}
216+
for await result in group.prefix(1) {
217+
timedOut = !result
218+
group.cancelAll()
219+
}
220+
}
221+
return timedOut ? .timedOut : .success
222+
}

0 commit comments

Comments
 (0)