Last active
April 3, 2023 16:28
-
-
Save bradleymackey/a68b51c79c1616d5e450cae377e501b4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
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
Hi @EfraimB, this is because
second
is referencingtaskB
, but it's not a child task, so whensecond
is cancelled it does not automatically propagate this cancellation totaskB
.This can be seen in this more simple example:
Even though we immediately create and then cancel the new Task
both
,taskA
andtaskB
still run to completion!We can make our semantics clear by explicitly cancelling
taskB
when we detectsecond
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.
I found this thread on the Swift Forums rather useful.