Skip to content

Instantly share code, notes, and snippets.

@bradleymackey
Last active April 3, 2023 16:28
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save bradleymackey/a68b51c79c1616d5e450cae377e501b4 to your computer and use it in GitHub Desktop.
public extension Task where Success == Never, Failure == Never {
/// Blueprint for a task that should be run, but not yet.
struct Blueprint<Output> {
public var priority: TaskPriority
public var operation: @Sendable () async throws -> Output
public init(
priority: TaskPriority = .medium,
operation: @escaping @Sendable () async throws -> Output
) {
self.priority = priority
self.operation = operation
}
}
}
public extension Task where Success == Never, Failure == Never {
/// Race for the first result by any of the provided tasks.
///
/// This will return the first valid result or throw the first thrown error by any task.
static func race<Output>(firstResolved tasks: [Blueprint<Output>]) async throws -> Output {
assert(!tasks.isEmpty, "You must race at least 1 task.")
return try await withThrowingTaskGroup(of: Output.self) { group -> Output in
for task in tasks {
group.addTask(priority: task.priority) {
try await task.operation()
}
}
defer { group.cancelAll() }
if let firstToResolve = try await group.next() {
return firstToResolve
} else {
// There will be at least 1 task.
fatalError("At least 1 task should be scheduled.")
}
}
}
/// Race for the first valid value.
///
/// Ignores errors that may be thrown and waits for the first result.
/// If all tasks fail, returns `nil`.
static func race<Output>(firstValue tasks: [Blueprint<Output>]) async -> Output? {
return await withThrowingTaskGroup(of: Output.self) { group -> Output? in
for task in tasks {
group.addTask(priority: task.priority) {
try await task.operation()
}
}
defer { group.cancelAll() }
while let nextResult = await group.nextResult() {
switch nextResult {
case .failure:
continue
case .success(let result):
return result
}
}
// If all the racing tasks error, we will reach this point.
return nil
}
}
}
@bradleymackey
Copy link
Author

bradleymackey commented Jan 5, 2023

Hi @EfraimB, this is because second is referencing taskB, but it's not a child task, so when second is cancelled it does not automatically propagate this cancellation to taskB.

This can be seen in this more simple example:

let both = Task {
    try await taskA.value
    try await taskB.value
}

both.cancel()

Even though we immediately create and then cancel the new Task both, taskA and taskB still run to completion!

We can make our semantics clear by explicitly cancelling taskB when we detect second is cancelled, as per your example.
Unfortunately this is just due to the unstructured nature of referencing a non-child task within the operation of the task blueprint.

let second = Task.Blueprint {
    try await withTaskCancellationHandler {
        try await taskB.value
    } onCancel: {
        taskB.cancel()
        print("cancel B")
    }
}

I found this thread on the Swift Forums rather useful.

@EfraimB
Copy link

EfraimB commented Jan 5, 2023

Oh, got it. Very much appreciated! Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment