Skip to content

Commit 3744f75

Browse files
committed
cleanups
1 parent d0e0d42 commit 3744f75

File tree

1 file changed

+46
-26
lines changed

1 file changed

+46
-26
lines changed

proposals/0304-structured-concurrency.md

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ func chopVegetables() async throws -> [Vegetable] {
251251
// Create a new child task for each vegetable that needs to be
252252
// chopped.
253253
for v in rawVeggies {
254-
await group.add {
254+
await group.spawn {
255255
return v.chopped()
256256
}
257257
}
@@ -288,7 +288,7 @@ A detached task is represented by a task handle (in this case, `Task.Handle<Meal
288288
let dinner = try await dinnerHandle.get()
289289
```
290290

291-
Detached tasks run to completion even if there are no remaining uses of their task handle, so `runDetached` is suitable for operations for which the program does not need to observe completion. However, the task handle can be used to explicitly cancel the operation, e.g.,
291+
Detached tasks run to completion even if there are no remaining uses of their task handle, so `spawnDetached` is suitable for operations for which the program does not need to observe completion. However, the task handle can be used to explicitly cancel the operation, e.g.,
292292

293293
```swift
294294
dinnerHandle.cancel()
@@ -342,10 +342,10 @@ func chopVegetables() async throws -> [Vegetable] {
342342
var veggies: [Vegetable] = []
343343

344344
try await withTaskGroup(resultType: Vegetable.self) { group in
345-
await group.add {
345+
await group.spawn {
346346
return try await chop(Carrot()) // (1) throws UnfortunateAccidentWithKnifeError()
347347
}
348-
await group.add {
348+
await group.spawn {
349349
return try await chop(Onion()) // (2)
350350
}
351351

@@ -495,9 +495,9 @@ extension Task {
495495

496496
The `priority` operation queries the priority of the task.
497497

498-
Task priorities are set on task creation (e.g., `spawnDetached` or `Task.Group.add`) and can be escalated later, e.g., if a higher-priority task waits on the task handle of a lower-priority task.
498+
Task priorities are set on task creation (e.g., `spawnDetached` or `TaskGroup.spawn`) and can be escalated later, e.g., if a higher-priority task waits on the task handle of a lower-priority task.
499499

500-
The `currentPriority` operation queries the priority of the currently-executing task. Task priorities are set on task creation (e.g., `spawnDetached` or `Task.Group.add`) and can be escalated later, e.g., if a higher-priority task waits on the task handle of a lower-priority task.
500+
The `currentPriority` operation queries the priority of the currently-executing task. Task priorities are set on task creation (e.g., `spawnDetached` or `TaskGroup.spawn`) and can be escalated later, e.g., if a higher-priority task waits on the task handle of a lower-priority task.
501501

502502
#### Task handles
503503

@@ -562,14 +562,14 @@ A new, detached task can be created with the `spawnDetached` operation. The resu
562562
```swift
563563
/// Create a new, detached task that produces a value of type `T`.
564564
@discardableResult
565-
static func runDetached<T: Sendable>(
565+
static func spawnDetached<T: Sendable>(
566566
priority: Priority = .default,
567567
operation: @escaping @concurrent () async -> T
568568
) -> Task.Handle<T, Never>
569569

570570
/// Create a new, detached task that produces a value of type `T` or throws an error.
571571
@discardableResult
572-
static func runDetached<T: Sendable>(
572+
static func spawnDetached<T: Sendable>(
573573
priority: Priority = .default,
574574
operation: @escaping @concurrent () async throws -> T
575575
) -> Task.Handle<T, Error>
@@ -586,8 +586,9 @@ try await eat(mealHandle: dinnerHandle)
586586
```
587587

588588
By default, the new task will be initially scheduled on the default global
589-
concurrent executor. `runDetached` may optionally be given a `startingOn`
590-
executor on which to schedule the new task instead.
589+
concurrent executor. Once custom executors are introduced in another proposal,
590+
these will be able to take an executor parameter to determine on which executor
591+
to schedule the new task instead.
591592

592593
#### Cancellation
593594

@@ -881,7 +882,7 @@ struct TaskGroup<ChildTaskResult: Sendable> {
881882
}
882883
```
883884

884-
`Task.Group` has no public initializers; instead, an instance of `Task.Group` is passed in to the `body` function of `withTaskGroup`. This instance should not be copied out of the `body` function, because doing so can break the child task structure.
885+
`TaskGroup` has no public initializers; instead, an instance of `TaskGroup` is passed in to the `body` function of `withTaskGroup`. This instance should not be copied out of the `body` function, because doing so can break the child task structure.
885886

886887
> **Note**: Swift does not currently have a way to ensure that the task group passed into the `body` function is not copied elsewhere, so we therefore rely on programmer discipline in a similar manner to, e.g., [`Array.withUnsafeBufferPointer`](https://developer.apple.com/documentation/swift/array/2994771-withunsafebufferpointer). However, in the case of task groups, we can at least provide a runtime assertion if one attempts to use the task group instance after its corresponding scope has ended.
887888
@@ -937,6 +938,18 @@ The `next()` operation allows one to gather the results from the tasks that have
937938

938939
```swift
939940
extension TaskGroup: AsyncSequence {
941+
/// Wait for a task to complete and return the result it returned (or throw if the task
942+
/// exited with a thrown error), or else return `nil` when there are no tasks left in
943+
/// the group.
944+
mutating func next() async -> TaskResult? { ... }
945+
946+
/// Query whether the task group has any remaining tasks.
947+
var isEmpty: Bool { ... }
948+
}
949+
```
950+
951+
```swift
952+
extension ThrowingTaskGroup: AsyncSequence {
940953
/// Wait for a task to complete and return the result it returned (or throw if the task
941954
/// exited with a thrown error), or else return `nil` when there are no tasks left in
942955
/// the group.
@@ -954,12 +967,19 @@ extension TaskGroup: AsyncSequence {
954967
The `next()` operation may typically be used within a `while` loop to gather the results of all outstanding tasks in the group, e.g.,
955968

956969
```swift
970+
while let result = await group.next() {
971+
// some accumulation logic (e.g. sum += result)
972+
}
973+
974+
// OR
975+
957976
while let result = try await group.next() {
958977
// some accumulation logic (e.g. sum += result)
959978
}
960979
```
961980

962-
`Task.Group` also conforms to the [`AsyncSequence` protocol](https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md), allowing the child tasks' results to be iterated in a `for await` loop:
981+
`TaskGroup` also conforms to the [`AsyncSequence` protocol](https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md), allowing the child tasks' results to be iterated in a `for await` loop:
982+
963983
```swift
964984
for await result in group { // non-throwing TaskGroup
965985
// some accumulation logic (e.g. sum += result)
@@ -982,7 +1002,7 @@ func gather(first m: Int, of work: [Work]) async throws -> [WorkResult] {
9821002

9831003
return withTaskGroup(resultType: WorkResult.self) { group in
9841004
for w in work {
985-
await group.add { await w.doIt() } // spawn child tasks to perform the work
1005+
await group.spawn { await w.doIt() } // spawn child tasks to perform the work
9861006
}
9871007

9881008
var results: [WorkResult] = []
@@ -1009,7 +1029,7 @@ A group's cancellation state can be queried by reading the `isCancelled`
10091029
property.
10101030

10111031
```swift
1012-
extension Task.Group {
1032+
extension TaskGroup {
10131033
/// Cancel all the remaining tasks in the task group.
10141034
/// Any results, including errors thrown, are discarded.
10151035
///
@@ -1028,14 +1048,14 @@ For example:
10281048
func chopVegetables() async throws -> [Vegetable] {
10291049
var veggies: [Vegetable] = []
10301050

1031-
try await withTaskGroup(resultType: Vegetable.self) { group in
1051+
try await withThrowingTaskGroup(resultType: Vegetable.self) { group in
10321052
print(group.isCancelled) // prints false
10331053

1034-
await group.add {
1054+
await group.spawn {
10351055
group.cancelAll() // Cancel all work in the group
10361056
throw UnfortunateAccidentWithKnifeError()
10371057
}
1038-
await group.add {
1058+
await group.spawn {
10391059
return try await chop(Onion())
10401060
}
10411061

@@ -1045,7 +1065,7 @@ func chopVegetables() async throws -> [Vegetable] {
10451065
}
10461066
} catch {
10471067
print(group.isCancelled) // prints true now
1048-
let added = await group.add {
1068+
let added = await group.spawn {
10491069
return try await chop(SweetPotato())
10501070
}
10511071
print(added) // prints false, no child was added to the cancelled group
@@ -1080,12 +1100,12 @@ Changes after first review:
10801100
* Most usages of tasks are rather intended to go through the static functions/properties on Task which implicitly works on the current task.
10811101
* `Task.unsafeCurrent` becomes a top-level `withUnsafeCurrentTask { maybeUnsafeTask in }`
10821102
* This better explains the intended semantics of not escaping storing the unsafe task reference.
1083-
* Adopt `spawn` terminology for "spawning tasks"
1103+
* Adopt `spawn...` terminology for "spawning tasks"
10841104
* `spawnDetached` becomes `spawnDetached`
1085-
* `Task.Group`'s `group.add` becomes `group.spawn`
1105+
* `TaskGroup`'s `group.spawn` becomes `group.spawn`
10861106
* Creating a child task will eventually be `spawn <something>`
10871107
* Moving away from using `Task` as namespace for everything
1088-
* rename `Task.Group` to `TaskGroup`
1108+
* rename `TaskGroup` to `TaskGroup`, and introduce `ThrowingTaskGroup`
10891109
* make `Task.unsafeCurrent` a free function`withUnsafeCurrentTask`
10901110
* Task group type parameter renames: `TaskGroup<TaskResult>` becomes `ChildTaskResult` resulting in: `public func withTaskGroup<ChildTaskResult, GroupResult>(of childTaskResultType: ChildTaskResult.Type, returning returnType: GroupResult.Type = GroupResult.self, body: (inout TaskGroup<ChildTaskResult>) async throws -> GroupResult) async rethrows -> GroupResult` resulting in a more readable call site: `withTaskGroup(of: Int.self)` and optionally `withTaskGroup(of: Int.self, returning: Int.self)`
10911111
* For now remove `startingChildTasksOn` from `withTaskGroup` since this is only doable with Custom Executors which are pending review still.
@@ -1097,7 +1117,7 @@ Changes after first review:
10971117
* Factored `async let` into [its own proposal](https://github.com/DougGregor/swift-evolution/pull/50).
10981118
* `Task` becomes a `struct` with instance functions, introduction of `Task.current`, `Task.unsafeCurrent` and the `UnsafeCurrentTask` APIs
10991119
* `Task.Group` now conforms to [the `AsyncSequence` protocol](https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md).
1100-
* `spawnDetached` and `Task.Group.add` now accept [executor](https://github.com/apple/swift-evolution/pull/1257) arguments to specify where the newly-spawned tasks are initially scheduled.
1120+
* `spawnDetached` and `Task.group.spawn` now accept [executor](https://github.com/apple/swift-evolution/pull/1257) arguments to specify where the newly-spawned tasks are initially scheduled.
11011121
* Changes in the second pitch:
11021122
* Added a "desugaring" of `async let` to task groups and more motivation for the structured-concurrency parts of the design.
11031123
* Reflowed the entire proposal to focus on the general description of structured concurrency first, the programming model with syntax next, and then details of the language features and API design last.
@@ -1117,7 +1137,7 @@ Changes after first review:
11171137

11181138
The design of task groups intentionally avoids exposing any task handles (futures) for child tasks. This ensures that the structure of structured concurrency, where all child tasks complete before their parent task, is maintained. That helps various properties such as priorities, deadlines, and cancellation to propagate in a meaningful way down the task tree.
11191139

1120-
However, an alternative design would bring futures to the forefront. One could introduce an `runChild` counterpart to `runDetached` that creates a new child task (of the current task), and then retrieve the result of that child task using the provided `Task.Handle`. To ensure that child tasks complete before the scope exits, we would require some kind of scoping mechanism that provides similar behavior to task groups. For example, the `makeDinner` example would be something like:
1140+
However, an alternative design would bring futures to the forefront. One could introduce an `runChild` counterpart to `spawnDetached` that creates a new child task (of the current task), and then retrieve the result of that child task using the provided `Task.Handle`. To ensure that child tasks complete before the scope exits, we would require some kind of scoping mechanism that provides similar behavior to task groups. For example, the `makeDinner` example would be something like:
11211141

11221142
```swift
11231143
func makeDinner() async throws -> Meal {
@@ -1152,13 +1172,13 @@ func makeDinner() async throws -> Meal {
11521172

11531173
// Create a task group to scope the lifetime of our three child tasks
11541174
try await withTaskGroup(resultType: Void.self) { group in
1155-
await group.add {
1175+
await group.spawn {
11561176
veggies = try await chopVegetables()
11571177
}
1158-
await group.add {
1178+
await group.spawn {
11591179
meat = await marinateMeat()
11601180
}
1161-
await group.app {
1181+
await group.spawn {
11621182
oven = await preheatOven(temperature: 350)
11631183
}
11641184
}

0 commit comments

Comments
 (0)