Skip to content

Instantly share code, notes, and snippets.

@bradleymackey
Last active January 5, 2023 08:44
Embed
What would you like to do?
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
}
}
}
@EfraimB
Copy link

EfraimB commented Jan 4, 2023

Doesn't seem to work when the operations wrap values of tasks.

        let taskA = Task {
            try await Task.sleep(seconds:2)
            print("A")
        }

        let taskB = Task {
            try await Task.sleep(seconds:7)
            print("B")
        }
        
        let first = Task<Never, Never>.Blueprint {
            try await taskA.value
        }
    
        let second = Task<Never, Never>.Blueprint {
            try await taskB.value
        }
        
        Task<Void, Error> {
            do {
                try await Task.race(firstResolved: [first, second])
                print("Finished all")
            } catch { error
                print(error)
            }
        }

This prints "A" and after 7 seconds "B" and "Finished All". Any ideas why this happens?

@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