Skip to content

Commit a5ac6f0

Browse files
committed
[Concurrency] detach, spawnUnlessCancelled, priority param cleanup
1 parent 34f8e76 commit a5ac6f0

15 files changed

+178
-130
lines changed

include/swift/Runtime/Concurrency.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
117117
JobPriority
118118
swift_task_escalate(AsyncTask *task, JobPriority newPriority);
119119

120+
// TODO: "async let wait" and "async let destroy" would be expressed
121+
// similar to like TaskFutureWait;
122+
120123
/// This matches the ABI of a closure `<T>(Builtin.NativeObject) async -> T`
121124
using TaskFutureWaitSignature =
122125
SWIFT_CC(swiftasync)
@@ -237,11 +240,12 @@ void swift_taskGroup_destroy(TaskGroup *group);
237240
///
238241
/// \code
239242
/// func swift_taskGroup_addPending(
240-
/// group: Builtin.RawPointer
243+
/// group: Builtin.RawPointer,
244+
/// unconditionally: Bool
241245
/// ) -> Bool
242246
/// \endcode
243247
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
244-
bool swift_taskGroup_addPending(TaskGroup *group);
248+
bool swift_taskGroup_addPending(TaskGroup *group, bool unconditionally);
245249

246250
/// Cancel all tasks in the group.
247251
/// This also prevents new tasks from being added.

lib/SILGen/SILGenDecl.cpp

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,8 @@ void SILGenFunction::emitPatternBinding(PatternBindingDecl *PBD,
11421142
auto initialization = emitPatternBindingInitialization(PBD->getPattern(idx),
11431143
JumpDest::invalid());
11441144

1145+
// TODO: need to allocate the variable, stackalloc it? pass the address to the start()
1146+
11451147
// If this is an async let, create a child task to compute the initializer
11461148
// value.
11471149
if (PBD->isAsyncLet()) {
@@ -1157,11 +1159,35 @@ void SILGenFunction::emitPatternBinding(PatternBindingDecl *PBD,
11571159
"Could not find async let autoclosure");
11581160
bool isThrowing = init->getType()->castTo<AnyFunctionType>()->isThrowing();
11591161

1162+
// TODO: there's a builtin to make an address into a raw pointer
1163+
// --- note dont need that; just have the builtin take it inout?
1164+
// --- the builtin can take the address (for start())
1165+
1166+
// TODO: make a builtin start async let
1167+
// Builtin.startAsyncLet -- and in the builtin create the async let record
1168+
1169+
// TODO: make a builtin for end async let
1170+
1171+
// TODO: IRGen would make a local allocation for the builtins
1172+
1173+
// TODO: remember if we did an await already?
1174+
1175+
// TODO: force in typesystem that we always await; then end aysnc let does not have to be async
1176+
// the local let variable is actually owning the result
1177+
// - but since throwing we can't know; maybe we didnt await on a thing yet
1178+
// so we do need the tracking if we waited on a thing
1179+
1180+
// TODO: awaiting an async let should be able to take ownership
1181+
// that means we will not await on this async let again, maybe?
1182+
// it means that the async let destroy should not destroy the result anymore
1183+
11601184
// Emit the closure for the child task.
11611185
SILValue childTask;
11621186
{
11631187
FullExpr Scope(Cleanups, CleanupLocation(init));
11641188
SILLocation loc(PBD);
1189+
// TODO: opaque object in the async context that represents the async let
1190+
//
11651191
childTask = emitRunChildTask(
11661192
loc,
11671193
init->getType(),
@@ -1173,7 +1199,7 @@ void SILGenFunction::emitPatternBinding(PatternBindingDecl *PBD,
11731199
enterDestroyCleanup(childTask);
11741200

11751201
// Push a cleanup that will cancel the child task at the end of the scope.
1176-
enterCancelAsyncTaskCleanup(childTask);
1202+
enterCancelAsyncTaskCleanup(childTask); // TODO: this is "went out scope" rather than just a cancel
11771203

11781204
// Save the child task so we can await it as needed.
11791205
AsyncLetChildTasks[{PBD, idx}] = { childTask, isThrowing };

stdlib/public/CompatibilityOverride/CompatibilityOverrideConcurrency.def

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ OVERRIDE_TASK_GROUP(taskGroup_cancelAll, void,
180180

181181
OVERRIDE_TASK_GROUP(taskGroup_addPending, bool,
182182
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),
183-
swift::, (TaskGroup *group), (group))
183+
swift::, (TaskGroup *group, bool unconditionally),
184+
(group, unconditionally))
184185

185186
OVERRIDE_TASK_LOCAL(task_localValuePush, void,
186187
SWIFT_EXPORT_FROM(swift_Concurrency), SWIFT_CC(swift),

stdlib/public/Concurrency/Task.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ extension Task {
396396
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
397397
@discardableResult
398398
public func detach<T>(
399-
priority: Task.Priority = .default,
399+
priority: Task.Priority = .unspecified,
400400
operation: __owned @Sendable @escaping () async -> T
401401
) -> Task.Handle<T, Never> {
402402
// Set up the job flags for a new task.
@@ -448,7 +448,7 @@ public func detach<T>(
448448
/// throw the error the operation has thrown when awaited on.
449449
@discardableResult
450450
public func detach<T, Failure>(
451-
priority: Task.Priority = .default,
451+
priority: Task.Priority = .unspecified,
452452
operation: __owned @Sendable @escaping () async throws -> T
453453
) -> Task.Handle<T, Failure> {
454454
// Set up the job flags for a new task.
@@ -470,7 +470,7 @@ public func detach<T, Failure>(
470470

471471
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
472472
// TODO: remove this?
473-
public func _runAsyncHandler(operation: @escaping () async -> ()) {
473+
func _runAsyncHandler(operation: @escaping () async -> ()) {
474474
typealias ConcurrentFunctionType = @Sendable () async -> ()
475475
detach(
476476
operation: unsafeBitCast(operation, to: ConcurrentFunctionType.self)

stdlib/public/Concurrency/TaskGroup.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,13 +361,19 @@ class TaskGroupImpl: public TaskGroupTaskStatusRecord {
361361
/// is currently executing the group. Here we only need the counts of
362362
/// pending/ready tasks.
363363
///
364+
/// If the `unconditionally` parameter is `true` the operation always successfully
365+
/// adds a pending task, even if the group is cancelled. If the unconditionally
366+
/// flag is `false`, the added pending count will be *reverted* before returning.
367+
/// This is because we will NOT add a task to a cancelled group, unless doing
368+
/// so unconditionally.
369+
///
364370
/// Returns *assumed* new status, including the just performed +1.
365-
GroupStatus statusAddPendingTaskRelaxed() {
371+
GroupStatus statusAddPendingTaskRelaxed(bool unconditionally) {
366372
auto old = status.fetch_add(GroupStatus::onePendingTask,
367373
std::memory_order_relaxed);
368374
auto s = GroupStatus{old + GroupStatus::onePendingTask};
369375

370-
if (s.isCancelled()) {
376+
if (!unconditionally && s.isCancelled()) {
371377
// revert that add, it was meaningless
372378
auto o = status.fetch_sub(GroupStatus::onePendingTask,
373379
std::memory_order_relaxed);
@@ -821,8 +827,8 @@ bool TaskGroupImpl::cancelAll() {
821827
// =============================================================================
822828
// ==== addPending -------------------------------------------------------------
823829
SWIFT_CC(swift)
824-
static bool swift_taskGroup_addPendingImpl(TaskGroup *group) {
825-
auto assumedStatus = asImpl(group)->statusAddPendingTaskRelaxed();
830+
static bool swift_taskGroup_addPendingImpl(TaskGroup *group, bool unconditionally) {
831+
auto assumedStatus = asImpl(group)->statusAddPendingTaskRelaxed(unconditionally);
826832
return !assumedStatus.isCancelled();
827833
}
828834

stdlib/public/Concurrency/TaskGroup.swift

Lines changed: 102 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -220,22 +220,17 @@ public struct TaskGroup<ChildTaskResult: Sendable> {
220220
/// - Returns:
221221
/// - `true` if the operation was added to the group successfully,
222222
/// `false` otherwise (e.g. because the group `isCancelled`)
223-
@discardableResult
224223
public mutating func spawn(
225-
overridingPriority priorityOverride: Task.Priority? = nil,
224+
priority: Task.Priority = .unspecified,
226225
operation: __owned @Sendable @escaping () async -> ChildTaskResult
227-
) -> Self.Spawned {
228-
let canAdd = _taskGroupAddPendingTask(group: _group)
229-
230-
guard canAdd else {
231-
// the group is cancelled and is not accepting any new work
232-
return Spawned(handle: nil)
233-
}
226+
) {
227+
_ = _taskGroupAddPendingTask(group: _group, unconditionally: true)
234228

235229
// Set up the job flags for a new task.
236230
var flags = Task.JobFlags()
237231
flags.kind = .task
238-
flags.priority = priorityOverride ?? getJobFlags(_task).priority
232+
flags.priority = priority != Task.Priority.unspecified ?
233+
priority : getJobFlags(_task).priority
239234
flags.isFuture = true
240235
flags.isChildTask = true
241236
flags.isGroupChildTask = true
@@ -249,25 +244,56 @@ public struct TaskGroup<ChildTaskResult: Sendable> {
249244

250245
// Enqueue the resulting job.
251246
_enqueueJobGlobal(Builtin.convertTaskToJob(childTask))
252-
253-
return Spawned(handle: Task.Handle(childTask))
254247
}
255248

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>?
249+
/// Add a child task to the group.
250+
///
251+
/// ### Error handling
252+
/// Operations are allowed to `throw`, in which case the `try await next()`
253+
/// invocation corresponding to the failed task will re-throw the given task.
254+
///
255+
/// The `add` function will never (re-)throw errors from the `operation`.
256+
/// Instead, the corresponding `next()` call will throw the error when necessary.
257+
///
258+
/// - Parameters:
259+
/// - overridingPriority: override priority of the operation task
260+
/// - operation: operation to execute and add to the group
261+
/// - Returns:
262+
/// - `true` if the operation was added to the group successfully,
263+
/// `false` otherwise (e.g. because the group `isCancelled`)
264+
public mutating func spawnUnlessCancelled(
265+
priority: Task.Priority = .unspecified,
266+
operation: __owned @Sendable @escaping () async -> ChildTaskResult
267+
) -> Bool {
268+
let canAdd = _taskGroupAddPendingTask(group: _group, unconditionally: false)
265269

266-
init(handle: Task.Handle<ChildTaskResult, Never>?) {
267-
self.handle = handle
270+
guard canAdd else {
271+
// the group is cancelled and is not accepting any new work
272+
return false
268273
}
274+
275+
// Set up the job flags for a new task.
276+
var flags = Task.JobFlags()
277+
flags.kind = .task
278+
flags.priority = priority != Task.Priority.unspecified ?
279+
priority : getJobFlags(_task).priority
280+
flags.isFuture = true
281+
flags.isChildTask = true
282+
flags.isGroupChildTask = true
283+
284+
// Create the asynchronous task future.
285+
let (childTask, _) = Builtin.createAsyncTaskGroupFuture(
286+
flags.bits, _group, operation)
287+
288+
// Attach it to the group's task record in the current task.
289+
_ = _taskGroupAttachChild(group: _group, child: childTask)
290+
291+
// Enqueue the resulting job.
292+
_enqueueJobGlobal(Builtin.convertTaskToJob(childTask))
293+
294+
return true
269295
}
270-
296+
271297
/// Wait for the a child task that was added to the group to complete,
272298
/// and return (or rethrow) the value it completed with. If no tasks are
273299
/// pending in the task group this function returns `nil`, allowing the
@@ -417,7 +443,7 @@ public struct ThrowingTaskGroup<ChildTaskResult: Sendable, Failure: Error> {
417443
self._group = group
418444
}
419445

420-
/// Add a child task to the group.
446+
/// Spawn, unconditionally, a child task in the group.
421447
///
422448
/// ### Error handling
423449
/// Operations are allowed to `throw`, in which case the `try await next()`
@@ -432,22 +458,64 @@ public struct ThrowingTaskGroup<ChildTaskResult: Sendable, Failure: Error> {
432458
/// - Returns:
433459
/// - `true` if the operation was added to the group successfully,
434460
/// `false` otherwise (e.g. because the group `isCancelled`)
435-
@discardableResult
436461
public mutating func spawn(
437-
overridingPriority priorityOverride: Task.Priority? = nil,
462+
priority: Task.Priority = .unspecified,
438463
operation: __owned @Sendable @escaping () async throws -> ChildTaskResult
439-
) -> Self.Spawned {
440-
let canAdd = _taskGroupAddPendingTask(group: _group)
464+
) {
465+
// we always add, so no need to check if group was cancelled
466+
_ = _taskGroupAddPendingTask(group: _group, unconditionally: true)
467+
468+
// Set up the job flags for a new task.
469+
var flags = Task.JobFlags()
470+
flags.kind = .task
471+
flags.priority = priority != Task.Priority.unspecified ?
472+
priority : getJobFlags(_task).priority
473+
flags.isFuture = true
474+
flags.isChildTask = true
475+
flags.isGroupChildTask = true
476+
477+
// Create the asynchronous task future.
478+
let (childTask, _) = Builtin.createAsyncTaskGroupFuture(
479+
flags.bits, _group, operation)
480+
481+
// Attach it to the group's task record in the current task.
482+
_ = _taskGroupAttachChild(group: _group, child: childTask)
483+
484+
// Enqueue the resulting job.
485+
_enqueueJobGlobal(Builtin.convertTaskToJob(childTask))
486+
}
487+
488+
/// Add a child task to the group.
489+
///
490+
/// ### Error handling
491+
/// Operations are allowed to `throw`, in which case the `try await next()`
492+
/// invocation corresponding to the failed task will re-throw the given task.
493+
///
494+
/// The `add` function will never (re-)throw errors from the `operation`.
495+
/// Instead, the corresponding `next()` call will throw the error when necessary.
496+
///
497+
/// - Parameters:
498+
/// - overridingPriority: override priority of the operation task
499+
/// - operation: operation to execute and add to the group
500+
/// - Returns:
501+
/// - `true` if the operation was added to the group successfully,
502+
/// `false` otherwise (e.g. because the group `isCancelled`)
503+
public mutating func spawnUnlessCancelled(
504+
priority: Task.Priority = .unspecified,
505+
operation: __owned @Sendable @escaping () async throws -> ChildTaskResult
506+
) -> Bool {
507+
let canAdd = _taskGroupAddPendingTask(group: _group, unconditionally: false)
441508

442509
guard canAdd else {
443510
// the group is cancelled and is not accepting any new work
444-
return Spawned(handle: nil)
511+
return false
445512
}
446513

447514
// Set up the job flags for a new task.
448515
var flags = Task.JobFlags()
449516
flags.kind = .task
450-
flags.priority = priorityOverride ?? getJobFlags(_task).priority
517+
flags.priority = priority != Task.Priority.unspecified ?
518+
priority : getJobFlags(_task).priority
451519
flags.isFuture = true
452520
flags.isChildTask = true
453521
flags.isGroupChildTask = true
@@ -462,22 +530,7 @@ public struct ThrowingTaskGroup<ChildTaskResult: Sendable, Failure: Error> {
462530
// Enqueue the resulting job.
463531
_enqueueJobGlobal(Builtin.convertTaskToJob(childTask))
464532

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-
}
533+
return true
481534
}
482535

483536
/// Wait for the a child task that was added to the group to complete,
@@ -754,7 +807,8 @@ func _taskGroupDestroy(group: __owned Builtin.RawPointer)
754807
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
755808
@_silgen_name("swift_taskGroup_addPending")
756809
func _taskGroupAddPendingTask(
757-
group: Builtin.RawPointer
810+
group: Builtin.RawPointer,
811+
unconditionally: Bool
758812
) -> Bool
759813

760814
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)

test/Concurrency/Runtime/async_task_detached.swift renamed to test/Concurrency/Runtime/async_task_detach.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class X {
2222
func test_detach() async {
2323
for _ in 1...3 {
2424
let x = X()
25-
let h = Task.runDetached {
25+
let h = detach {
2626
print("inside: \(x)")
2727
}
2828
await h.get()

test/Concurrency/Runtime/async_task_priority_current.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Dispatch
1212
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
1313
func test_detach() async {
1414
let a1 = Task.currentPriority
15-
print("a1: \(a1)") // CHECK: a1: default
15+
print("a1: \(a1)") // CHECK: a1: unspecified
1616

1717
// Note: remember to detach using a higher priority, otherwise a lower one
1818
// might be escalated by the get() and we could see `default` in the detached
@@ -23,7 +23,7 @@ func test_detach() async {
2323
}.get()
2424

2525
let a3 = Task.currentPriority
26-
print("a3: \(a3)") // CHECK: a3: default
26+
print("a3: \(a3)") // CHECK: a3: unspecified
2727
}
2828

2929
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)

0 commit comments

Comments
 (0)