11
11
////===----------------------------------------------------------------------===//
12
12
13
13
import Swift
14
+ import Dispatch
14
15
@_implementationOnly import _SwiftConcurrencyShims
15
16
16
17
// ==== Task Group -------------------------------------------------------------
@@ -32,6 +33,12 @@ extension Task {
32
33
/// // some accumulation logic (e.g. sum += result)
33
34
/// }
34
35
///
36
+ /// ### Thrown errors
37
+ /// When tasks are added to the group using the `group.add` function, they may
38
+ /// immediately begin executing. Even if their results are not collected explicitly
39
+ /// and such task throws, and was not yet cancelled, it may result in the `withGroup`
40
+ /// throwing.
41
+ ///
35
42
/// ### Cancellation
36
43
/// If an error is thrown out of the task group, all of its remaining tasks
37
44
/// will be cancelled and the `withGroup` call will rethrow that error.
@@ -57,61 +64,149 @@ extension Task {
57
64
public static func withGroup< TaskResult, BodyResult> (
58
65
resultType: TaskResult . Type ,
59
66
returning returnType: BodyResult . Type = BodyResult . self,
60
- body: ( inout Task . Group < TaskResult > ) async throws -> BodyResult
61
- ) async rethrows -> BodyResult {
62
- fatalError ( " \( #function) not implemented yet. " )
67
+ cancelOutstandingTasksOnReturn: Bool = false ,
68
+ body: @escaping ( ( inout Task . Group < TaskResult > ) async throws -> BodyResult )
69
+ ) async throws -> BodyResult {
70
+ let drainPendingTasksOnSuccessfulReturn = !cancelOutstandingTasksOnReturn
71
+ let parent = Builtin . getCurrentAsyncTask ( )
72
+
73
+ // Set up the job flags for a new task.
74
+ var groupFlags = JobFlags ( )
75
+ groupFlags. kind = . task // TODO: .taskGroup?
76
+ groupFlags. priority = . default // TODO: parent's priority // await Task.currentPriority()
77
+ groupFlags. isFuture = true
78
+
79
+ // 1. Prepare the Group task
80
+ // FIXME: do we have to rather prepare it inside the task we spawn; and yield it back along with the result instead?
81
+ var group = Task . Group< TaskResult> ( parentTask: parent)
82
+
83
+ let ( groupTask, context) =
84
+ Builtin . createAsyncTaskFuture ( groupFlags. bits, nil ) { ( ) async throws -> BodyResult in
85
+ let got = await try body ( & group)
86
+ return got
87
+ }
88
+ let groupHandle = Handle < BodyResult > ( task: groupTask)
89
+
90
+ // 2.0) Run the task!
91
+ DispatchQueue . global ( priority: . default) . async {
92
+ groupHandle. run ( ) // TODO: this synchronously runs
93
+ }
94
+
95
+ // 2.1) ensure that if we fail and exit by throwing we will cancel all tasks,
96
+ // if we succeed, there is nothing to cancel anymore so this is noop
97
+ defer { group. cancelAll ( ) }
98
+
99
+ // 2.2) Await the group completing it's run ("until the withGroup returns")
100
+ let result = await try groupHandle. get ( ) // if we throw, so be it -- group tasks will be cancelled
101
+
102
+ // TODO: do drain before exiting
103
+ // if drainPendingTasksOnSuccessfulReturn {
104
+ // // drain all outstanding tasks
105
+ // while await try group.next() != nil {
106
+ // continue // awaiting all remaining tasks
107
+ // }
108
+ // }
109
+
110
+ return result
63
111
}
64
112
65
113
/// A task group serves as storage for dynamically started tasks.
66
114
///
67
- /// Its intended use is with the
115
+ /// Its intended use is with the `Task.withGroup` function.
68
116
/* @unmoveable */
69
117
public struct Group < TaskResult> {
118
+ private let parentTask : Builtin . NativeObject
119
+
120
+ // TODO: we want groups to be unordered in completion, the counterpart to streams (Series),
121
+ // as such it feels like we need to keep them like this, because a next() can complete any of them
122
+ // and then we need to remove it from the pending ones
123
+ private var pendingTasks : [ Int : Handle < TaskResult > ] // TODO: make a dict for out of order completions
124
+ private var nextTaskID : Int = 0
125
+
126
+ /// If present, the handle on which the `next()` call is awaiting,
127
+ /// it should be resumed by *any* of the in-flight tasks completing.
128
+ private var nextHandle : Task . Handle < TaskResult > ? = nil
129
+
70
130
/// No public initializers
71
- private init ( ) { }
131
+ init ( parentTask: Builtin . NativeObject ) {
132
+ self . parentTask = parentTask
133
+ self . pendingTasks = [ : ]
134
+ }
72
135
73
136
// Swift will statically prevent this type from being copied or moved.
74
137
// For now, that implies that it cannot be used with generics.
75
138
76
139
/// Add a child task to the group.
77
140
///
78
141
/// ### Error handling
79
- /// Operations are allowed to throw.
80
- ///
81
- /// in which case the `await try next()`
142
+ /// Operations are allowed to `throw`, in which case the `await try next()`
82
143
/// invocation corresponding to the failed task will re-throw the given task.
83
144
///
145
+ /// The `add` function will never (re)-throw exceptions from the `operation`,
146
+ /// the corresponding `next()` call will throw the error when necessary.
147
+ ///
84
148
/// - Parameters:
85
149
/// - overridingPriority: override priority of the operation task
86
150
/// - operation: operation to execute and add to the group
151
+ @discardableResult
87
152
public mutating func add(
88
153
overridingPriority: Priority ? = nil ,
89
- operation: ( ) async throws -> TaskResult
90
- ) async {
91
- fatalError ( " \( #function) not implemented yet. " )
92
- }
154
+ operation: @escaping ( ) async throws -> TaskResult
155
+ ) async -> Task . Handle < TaskResult > {
156
+ var flags = JobFlags ( )
157
+ flags. kind = . task // TODO: childTask?
158
+ flags. priority = . default // TODO: priority getting from parent not implemented yet
159
+ // if let overridingPriority = overridingPriority { // TODO: cannot use ?? with async defaultValue
160
+ // flags.priority = overridingPriority
161
+ // } else {
162
+ // flags.priority = await Task.currentPriority() // TODO: self.parent.priority ?
163
+ // }
164
+ flags. isFuture = true
93
165
94
- /// Add a child task and return a `Task.Handle` that can be used to manage it.
95
- ///
96
- /// The task's result is accessible either via the returned `handle` or the
97
- /// `group.next()` function (as any other `add`-ed task).
98
- ///
99
- /// - Parameters:
100
- /// - overridingPriority: override priority of the operation task
101
- /// - operation: operation to execute and add to the group
102
- public mutating func addWithHandle(
103
- overridingPriority: Priority ? = nil ,
104
- operation: ( ) async throws -> TaskResult
105
- ) async -> Handle < TaskResult > {
106
- fatalError ( " \( #function) not implemented yet. " )
166
+ let ( childTask, context) =
167
+ // TODO: passing the parentTask (instead of nil) here makes the program hang here
168
+ Builtin . createAsyncTaskFuture ( flags. bits, nil , operation)
169
+
170
+ let handle = Handle < TaskResult > ( task: childTask)
171
+
172
+ // runTask(childTask)
173
+ DispatchQueue . global ( priority: . default) . async {
174
+ handle. run ( )
175
+ }
176
+
177
+ // _ = DispatchQueue.global(priority: .default).async {
178
+ // print("run dispatch INSIDE: \(childTask)")
179
+ // await try operation()
180
+ // }
181
+
182
+ // FIXME: need to store? self.pendingTasks[ObjectIdentifier(childTask)] = childTask
183
+
184
+ defer { nextTaskID += 1 }
185
+ self . pendingTasks [ nextTaskID] = handle
186
+
187
+ return handle
107
188
}
108
189
109
190
/// Wait for a child task to complete and return the result it returned,
110
191
/// or else return.
111
192
///
112
- ///
113
- public mutating func next( ) async throws -> TaskResult ? {
114
- fatalError ( " \( #function) not implemented yet. " )
193
+ /// Order of completions is *not* guaranteed to be same as submission order,
194
+ /// rather the order of `next()` calls completing is by completion order of
195
+ /// the tasks. This differentiates task groups from streams (
196
+ public mutating func next( file: String = #file, line: UInt = #line) async throws -> TaskResult ? {
197
+ // FIXME: this implementation is wrong and naive; we instead need to maintain a dict of handles,
198
+ // and return them as they complete in that order; so likely a queue of "which one completed"
199
+ // this will allow building "collect first N results" APIs easily;
200
+ // APIs which need order can implement on top of this, or we provide a different API for it
201
+ let handle = self . pendingTasks. removeValue ( forKey: 0 ) ??
202
+ self . pendingTasks. removeValue ( forKey: 1 )
203
+
204
+ if let handle = handle {
205
+ let got = await try handle. get ( )
206
+ return got
207
+ } else {
208
+ return nil
209
+ }
115
210
}
116
211
117
212
/// Query whether the group has any remaining tasks.
@@ -122,7 +217,7 @@ extension Task {
122
217
///
123
218
/// - Returns: `true` if the group has no pending tasks, `false` otherwise.
124
219
public var isEmpty : Bool {
125
- fatalError ( " \( #function ) not implemented yet. " )
220
+ return self . pendingTasks . isEmpty
126
221
}
127
222
128
223
/// Cancel all the remaining tasks in the group.
@@ -133,8 +228,11 @@ extension Task {
133
228
/// cancellation, are silently discarded.
134
229
///
135
230
/// - SeeAlso: `Task.addCancellationHandler`
136
- public mutating func cancelAll( ) {
137
- fatalError ( " \( #function) not implemented yet. " )
231
+ public mutating func cancelAll( file: String = #file, line: UInt = #line) {
232
+ for (id, handle) in self . pendingTasks {
233
+ handle. cancel ( )
234
+ }
235
+ self . pendingTasks = [ : ]
138
236
}
139
237
}
140
238
}
0 commit comments