Skip to content

Commit 34f8e76

Browse files
committed
spawnDetached to detach, non-suspending group.spawn, spawnUnlessCancelled
1 parent 31144b9 commit 34f8e76

39 files changed

+262
-136
lines changed

stdlib/public/Concurrency/Task.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ extension Task {
105105
/// ### Priority inheritance
106106
/// Child tasks automatically inherit their parent task's priority.
107107
///
108-
/// Detached tasks (created by `spawnDetached`) DO NOT inherit task priority,
108+
/// Detached tasks (created by `detach`) DO NOT inherit task priority,
109109
/// as they are "detached" from their parent tasks after all.
110110
///
111111
/// ### Priority elevation
@@ -150,7 +150,7 @@ extension Task {
150150
/// i.e. the task will run regardless of the handle still being present or not.
151151
/// Dropping a handle however means losing the ability to await on the task's result
152152
/// and losing the ability to cancel it.
153-
public struct Handle<Success, Failure: Error> {
153+
public struct Handle<Success, Failure: Error>: Sendable {
154154
internal let _task: Builtin.NativeObject
155155

156156
internal init(_ task: Builtin.NativeObject) {
@@ -395,7 +395,7 @@ extension Task {
395395
/// throw the error the operation has thrown when awaited on.
396396
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
397397
@discardableResult
398-
public func spawnDetached<T>(
398+
public func detach<T>(
399399
priority: Task.Priority = .default,
400400
operation: __owned @Sendable @escaping () async -> T
401401
) -> Task.Handle<T, Never> {
@@ -447,7 +447,7 @@ public func spawnDetached<T>(
447447
/// tasks result or `cancel` it. If the operation fails the handle will
448448
/// throw the error the operation has thrown when awaited on.
449449
@discardableResult
450-
public func spawnDetached<T, Failure>(
450+
public func detach<T, Failure>(
451451
priority: Task.Priority = .default,
452452
operation: __owned @Sendable @escaping () async throws -> T
453453
) -> Task.Handle<T, Failure> {
@@ -472,7 +472,7 @@ public func spawnDetached<T, Failure>(
472472
// TODO: remove this?
473473
public func _runAsyncHandler(operation: @escaping () async -> ()) {
474474
typealias ConcurrentFunctionType = @Sendable () async -> ()
475-
spawnDetached(
475+
detach(
476476
operation: unsafeBitCast(operation, to: ConcurrentFunctionType.self)
477477
)
478478
}
@@ -629,7 +629,7 @@ public func _asyncMainDrainQueue() -> Never
629629
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
630630
public func _runAsyncMain(_ asyncFun: @escaping () async throws -> ()) {
631631
#if os(Windows)
632-
spawnDetached {
632+
detach {
633633
do {
634634
try await asyncFun()
635635
exit(0)
@@ -647,7 +647,7 @@ public func _runAsyncMain(_ asyncFun: @escaping () async throws -> ()) {
647647
}
648648
}
649649

650-
spawnDetached {
650+
detach {
651651
await _doMain(asyncFun)
652652
exit(0)
653653
}
@@ -705,12 +705,12 @@ func _taskIsCancelled(_ task: Builtin.NativeObject) -> Bool
705705
@_alwaysEmitIntoClient
706706
@usableFromInline
707707
internal func _runTaskForBridgedAsyncMethod(_ body: @escaping () async -> Void) {
708-
// TODO: We can probably do better than spawnDetached
708+
// TODO: We can probably do better than detach
709709
// if we're already running on behalf of a task,
710710
// if the receiver of the method invocation is itself an Actor, or in other
711711
// situations.
712712
#if compiler(>=5.5) && $Sendable
713-
spawnDetached { await body() }
713+
detach { await body() }
714714
#endif
715715
}
716716

stdlib/public/Concurrency/TaskGroup.swift

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,8 @@ import Swift
6161
/// - if the body returns normally:
6262
/// - the group will await any not yet complete tasks,
6363
/// - once the `withTaskGroup` returns the group is guaranteed to be empty.
64-
/// - if the body throws:
65-
/// - all tasks remaining in the group will be automatically cancelled.
6664
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
67-
public func withTaskGroup<ChildTaskResult, GroupResult>(
65+
public func withTaskGroup<ChildTaskResult: Sendable, GroupResult>(
6866
of childTaskResultType: ChildTaskResult.Type,
6967
returning returnType: GroupResult.Type = GroupResult.self,
7068
body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
@@ -144,7 +142,7 @@ public func withTaskGroup<ChildTaskResult, GroupResult>(
144142
/// - once the `withTaskGroup` returns the group is guaranteed to be empty.
145143
/// - if the body throws:
146144
/// - all tasks remaining in the group will be automatically cancelled.
147-
public func withThrowingTaskGroup<ChildTaskResult, GroupResult>(
145+
public func withThrowingTaskGroup<ChildTaskResult: Sendable, GroupResult>(
148146
of childTaskResultType: ChildTaskResult.Type,
149147
returning returnType: GroupResult.Type = GroupResult.self,
150148
body: (inout ThrowingTaskGroup<ChildTaskResult, Error>) async throws -> GroupResult
@@ -191,7 +189,7 @@ public func withThrowingTaskGroup<ChildTaskResult, GroupResult>(
191189
/// A task group serves as storage for dynamically spawned tasks.
192190
///
193191
/// It is created by the `withTaskGroup` function.
194-
public struct TaskGroup<ChildTaskResult> {
192+
public struct TaskGroup<ChildTaskResult: Sendable> {
195193

196194
private let _task: Builtin.NativeObject
197195
/// Group task into which child tasks offer their results,
@@ -225,13 +223,13 @@ public struct TaskGroup<ChildTaskResult> {
225223
@discardableResult
226224
public mutating func spawn(
227225
overridingPriority priorityOverride: Task.Priority? = nil,
228-
operation: @Sendable @escaping () async -> ChildTaskResult
229-
) async -> Bool {
226+
operation: __owned @Sendable @escaping () async -> ChildTaskResult
227+
) -> Self.Spawned {
230228
let canAdd = _taskGroupAddPendingTask(group: _group)
231229

232230
guard canAdd else {
233231
// the group is cancelled and is not accepting any new work
234-
return false
232+
return Spawned(handle: nil)
235233
}
236234

237235
// Set up the job flags for a new task.
@@ -252,7 +250,22 @@ public struct TaskGroup<ChildTaskResult> {
252250
// Enqueue the resulting job.
253251
_enqueueJobGlobal(Builtin.convertTaskToJob(childTask))
254252

255-
return true
253+
return Spawned(handle: Task.Handle(childTask))
254+
}
255+
256+
public struct Spawned: Sendable {
257+
/// Returns `true` if the task was successfully spawned in the task group,
258+
/// `false` otherwise which means that the group was already cancelled and
259+
/// refused to accept spawn a new child task.
260+
public var successfully: Bool { handle != nil }
261+
262+
/// Task handle for the spawned task group child task,
263+
/// or `nil` if it was not spawned successfully.
264+
public let handle: Task.Handle<ChildTaskResult, Never>?
265+
266+
init(handle: Task.Handle<ChildTaskResult, Never>?) {
267+
self.handle = handle
268+
}
256269
}
257270

258271
/// Wait for the a child task that was added to the group to complete,
@@ -296,8 +309,8 @@ public struct TaskGroup<ChildTaskResult> {
296309
/// Order of values returned by next() is *completion order*, and not
297310
/// submission order. I.e. if tasks are added to the group one after another:
298311
///
299-
/// await group.spawn { 1 }
300-
/// await group.spawn { 2 }
312+
/// group.spawn { 1 }
313+
/// group.spawn { 2 }
301314
///
302315
/// print(await group.next())
303316
/// /// Prints "1" OR "2"
@@ -388,7 +401,7 @@ public struct TaskGroup<ChildTaskResult> {
388401
/// child tasks.
389402
///
390403
/// It is created by the `withTaskGroup` function.
391-
public struct ThrowingTaskGroup<ChildTaskResult, Failure: Error> {
404+
public struct ThrowingTaskGroup<ChildTaskResult: Sendable, Failure: Error> {
392405

393406
private let _task: Builtin.NativeObject
394407
/// Group task into which child tasks offer their results,
@@ -423,12 +436,12 @@ public struct ThrowingTaskGroup<ChildTaskResult, Failure: Error> {
423436
public mutating func spawn(
424437
overridingPriority priorityOverride: Task.Priority? = nil,
425438
operation: __owned @Sendable @escaping () async throws -> ChildTaskResult
426-
) async -> Bool {
439+
) -> Self.Spawned {
427440
let canAdd = _taskGroupAddPendingTask(group: _group)
428441

429442
guard canAdd else {
430443
// the group is cancelled and is not accepting any new work
431-
return false
444+
return Spawned(handle: nil)
432445
}
433446

434447
// Set up the job flags for a new task.
@@ -449,7 +462,22 @@ public struct ThrowingTaskGroup<ChildTaskResult, Failure: Error> {
449462
// Enqueue the resulting job.
450463
_enqueueJobGlobal(Builtin.convertTaskToJob(childTask))
451464

452-
return true
465+
return Spawned(handle: Task.Handle(childTask))
466+
}
467+
468+
public struct Spawned: Sendable {
469+
/// Returns `true` if the task was successfully spawned in the task group,
470+
/// `false` otherwise which means that the group was already cancelled and
471+
/// refused to accept spawn a new child task.
472+
public var successfully: Bool { handle != nil }
473+
474+
/// Task handle for the spawned task group child task,
475+
/// or `nil` if it was not spawned successfully.
476+
public let handle: Task.Handle<ChildTaskResult, Error>?
477+
478+
init(handle: Task.Handle<ChildTaskResult, Error>?) {
479+
self.handle = handle
480+
}
453481
}
454482

455483
/// Wait for the a child task that was added to the group to complete,
@@ -493,8 +521,8 @@ public struct ThrowingTaskGroup<ChildTaskResult, Failure: Error> {
493521
/// Order of values returned by next() is *completion order*, and not
494522
/// submission order. I.e. if tasks are added to the group one after another:
495523
///
496-
/// await group.spawn { 1 }
497-
/// await group.spawn { 2 }
524+
/// group.spawn { 1 }
525+
/// group.spawn { 2 }
498526
///
499527
/// print(await group.next())
500528
/// /// Prints "1" OR "2"
@@ -519,6 +547,29 @@ public struct ThrowingTaskGroup<ChildTaskResult, Failure: Error> {
519547
return try await _taskGroupWaitNext(group: _group)
520548
}
521549

550+
/// - SeeAlso: `next()`
551+
public mutating func nextResult() async throws -> Result<ChildTaskResult, Failure>? {
552+
#if NDEBUG
553+
let callingTask = Builtin.getCurrentAsyncTask() // can't inline into the assert sadly
554+
assert(unsafeBitCast(callingTask, to: size_t.self) ==
555+
unsafeBitCast(_task, to: size_t.self),
556+
"""
557+
group.next() invoked from task other than the task which created the group! \
558+
This means the group must have illegally escaped the withTaskGroup{} scope.
559+
""")
560+
#endif
561+
562+
do {
563+
guard let success: ChildTaskResult = try await _taskGroupWaitNext(group: _group) else {
564+
return nil
565+
}
566+
567+
return .success(success)
568+
} catch {
569+
return .failure(error as! Failure) // as!-safe, because we are only allowed to throw Failure (Error)
570+
}
571+
}
572+
522573
/// Query whether the group has any remaining tasks.
523574
///
524575
/// Task groups are always empty upon entry to the `withTaskGroup` body, and
@@ -650,6 +701,7 @@ extension ThrowingTaskGroup: AsyncSequence {
650701

651702
/// - SeeAlso: `ThrowingTaskGroup.next()` for a detailed discussion its semantics.
652703
public mutating func next() async throws -> Element? {
704+
guard !finished else { return nil }
653705
do {
654706
guard let element = try await group.next() else {
655707
finished = true

test/Concurrency/Runtime/actor_counters.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func runTest(numCounters: Int, numWorkers: Int, numIterations: Int) async {
5252
var workers: [Task.Handle<Void, Error>] = []
5353
for i in 0..<numWorkers {
5454
workers.append(
55-
spawnDetached { [counters] in
55+
detach { [counters] in
5656
await Task.sleep(UInt64.random(in: 0..<100) * 1_000_000)
5757
await worker(identity: i, counters: counters, numIterations: numIterations)
5858
}

test/Concurrency/Runtime/async_task_cancellation_early.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
import Dispatch
1111

1212
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
13-
func test_spawnDetached_cancel_child_early() async {
14-
print(#function) // CHECK: test_spawnDetached_cancel_child_early
15-
let h: Task.Handle<Bool, Error> = spawnDetached {
13+
func test_detach_cancel_child_early() async {
14+
print(#function) // CHECK: test_detach_cancel_child_early
15+
let h: Task.Handle<Bool, Error> = detach {
1616
async let childCancelled: Bool = { () -> Bool in
1717
await Task.sleep(2_000_000_000)
1818
return Task.isCancelled
@@ -37,6 +37,6 @@ func test_spawnDetached_cancel_child_early() async {
3737
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
3838
@main struct Main {
3939
static func main() async {
40-
await test_spawnDetached_cancel_child_early()
40+
await test_detach_cancel_child_early()
4141
}
4242
}

test/Concurrency/Runtime/async_task_cancellation_while_running.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import Dispatch
1111

1212
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
13-
func test_spawnDetached_cancel_while_child_running() async {
14-
let h: Task.Handle<Bool, Error> = spawnDetached {
13+
func test_detach_cancel_while_child_running() async {
14+
let h: Task.Handle<Bool, Error> = detach {
1515
async let childCancelled: Bool = { () -> Bool in
1616
await Task.sleep(3_000_000_000)
1717
return Task.isCancelled
@@ -36,6 +36,6 @@ func test_spawnDetached_cancel_while_child_running() async {
3636
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
3737
@main struct Main {
3838
static func main() async {
39-
await test_spawnDetached_cancel_while_child_running()
39+
await test_detach_cancel_while_child_running()
4040
}
4141
}

test/Concurrency/Runtime/async_task_equals_hashCode.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
1212
func simple() async {
1313
print("\(#function) -----------------------")
14-
let one = await Task.current()
15-
let two = await Task.current()
14+
let one = Task.current!
15+
let two = Task.current!
1616
print("same equal: \(one == two)") // CHECK: same equal: true
1717
print("hashes equal: \(one.hashValue == two.hashValue)") // CHECK: hashes equal: true
1818

19-
async let x = Task.current()
19+
async let x = Task.current
2020
let three = await x
2121

2222
print("parent/child equal: \(three == two)") // CHECK: parent/child equal: false
@@ -26,12 +26,12 @@ func simple() async {
2626
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
2727
func unsafe() async {
2828
print("\(#function) -----------------------")
29-
let one = Task.unsafeCurrent!
30-
let two = Task.unsafeCurrent!
29+
let one = withUnsafeCurrentTask { $0! }
30+
let two = withUnsafeCurrentTask { $0! }
3131
print("unsafe same equal: \(one == two)") // CHECK: same equal: true
3232
print("unsafe hashes equal: \(one.hashValue == two.hashValue)") // CHECK: hashes equal: true
3333

34-
async let x = Task.unsafeCurrent!
34+
async let x = withUnsafeCurrentTask { $0! }
3535
let three = await x
3636

3737
print("unsafe parent/child equal: \(three == two)") // CHECK: parent/child equal: false
@@ -44,8 +44,8 @@ func unsafe() async {
4444
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
4545
func unsafeSync() {
4646
print("\(#function) -----------------------")
47-
let one = Task.unsafeCurrent!
48-
let two = Task.unsafeCurrent!
47+
let one = withUnsafeCurrentTask { $0! }
48+
let two = withUnsafeCurrentTask { $0! }
4949
print("unsafe same equal: \(one == two)") // CHECK: same equal: true
5050
print("unsafe hashes equal: \(one.hashValue == two.hashValue)") // CHECK: hashes equal: true
5151
}

test/Concurrency/Runtime/async_task_handle_cancellation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
1313
@main struct Main {
1414
static func main() async {
15-
let handle = spawnDetached {
15+
let handle = detach {
1616
while (!Task.isCancelled) { // no need for await here, yay
1717
print("waiting")
1818
}

test/Concurrency/Runtime/async_task_locals_groups.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func groups() async {
5151

5252
// no value in parent, value in child
5353
let x1: Int = try! await withTaskGroup(of: Int.self) { group in
54-
await group.spawn {
54+
group.spawn {
5555
printTaskLocal(\.number) // CHECK: NumberKey: 0 {{.*}}
5656
// inside the child task, set a value
5757
await Task.withLocal(\.number, boundTo: 1) {
@@ -71,7 +71,7 @@ func groups() async {
7171

7272
let x2: Int = try! await withTaskGroup(of: Int.self) { group in
7373
printTaskLocal(\.number) // CHECK: NumberKey: 2 {{.*}}
74-
await group.spawn {
74+
group.spawn {
7575
printTaskLocal(\.number) // CHECK: NumberKey: 2 {{.*}}
7676

7777
async let childInsideGroupChild: () = printTaskLocal(\.number)

test/Concurrency/Runtime/async_task_locals_inherit_never.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@ func test_async_group() async {
6868
await Task.withLocal(\.string, boundTo: "top") {
6969
printTaskLocal(\.string) // CHECK: StringKey: top {{.*}}
7070

71-
try! await withTaskGroup(of: Void.self) { group -> Void? in
71+
await withTaskGroup(of: Int.self, returning: Void.self) { group in
7272
printTaskLocal(\.string) // CHECK: StringKey: top {{.*}}
7373

74-
await group.spawn {
74+
group.spawn {
7575
printTaskLocal(\.string) // CHECK: StringKey: <undefined> {{.*}}
76+
return 0
7677
}
7778

78-
return try! await group.next()
79+
_ = await group.next()
7980
}
8081
}
8182
}

0 commit comments

Comments
 (0)