I know the following is due to only partially using structured concurrency, but it's a problem I've stumbled across several times over the years and I have not yet found a simple answer with the tools Swift concurrency provides.
How do you cancel the wait for another top-level task without having to cancel the other top-level task?
Let's take a look at the following simple example using a standalone top-level task doing some expensive work:
let sharedTask = Task.detached {
// some long running expensive work
try await Task.sleep(for: .seconds(10))
return 42
}
The result of this task is to be used in various other asynchronous methods:
func addMagic() async throws -> Int {
let magicNumber = try await sharedTask.value
// do something with the number and potentially wait for other async methods before we return a result
try await Task.sleep(for: .seconds(1))
return magicNumber + 2
}
func multiplyMagic() async throws -> Int {
let magicNumber = try await sharedTask.value
// do something with the number and potentially wait for other async methods before we return a result
try await Task.sleep(for: .seconds(2))
return magicNumber * 2
}
These methods are otherwise used in a structured concurrency context and ultimately have a top-level task under which they are executed.
For the sake of simplicity, let's look at this as follows:
let task1 = Task {
defer { print("Task1 done") }
let result = try await addMagic()
print("The new magic number is \(result)")
}
let task2 = Task {
defer { print("Task2 done") }
let result = try await multiplyMagic()
print("The new magic number is \(result)")
}
The problem we get at this point is with cancellation behavior.
For example, if we try to cancel task2 with the cancel method of the task, the multiplyMagic method won't return until the sharedTask instance returns a value or is canceled.
// This should cause "Task2 done" to be printed immediately and not wait for the sharedTask to complete.
task2.cancel()
And since we are talking about a shared task whose ownership does not lie with the callers, the callers should not cancel the shared task just because they are canceled.
What would be desirable at this point is that when task2 is canceled, the wait for the value of sharedTask is canceled but not the sharedTask itself.
What I have done in these cases so far is to use methods from Combine and publish/subscriber patterns to achieve the appropriate result.
How do you achieve this behavior with the tools Swift Concurrency provides?
Or is the simple answer: this cannot be achieved with Swift Concurrency alone?