I have watched Explore structured concurrency in Swift
video and other relevant videos / articles / books I was able to find (swift by Sundell, hacking with swift, Ray Renderlich), but all examples there are very trivial – async functions usually only have 1 async call in them. How should this work in real life code?
For example:
...
task = Task {
var longRunningWorker: LongRunningWorker? = nil
do {
var fileURL = state.fileURL
if state.needsCompression {
longRunningWorker = LongRunningWorker(inputURL: fileURL)
fileURL = try await longRunningWorker!.doAsyncWork()
}
let urls = try await ApiService.i.fetchUploadUrls()
if let image = state.image, let imageData = image.jpegData(compressionQuality: 0.8) {
guard let imageUrl = urls.signedImageUrl else {
fatalError("Cover art supplied but art upload URL is nil")
}
try await ApiService.i.uploadData(url: imageUrl, data: imageData)
}
let fileData = try Data(contentsOf: state.fileUrl)
try await ApiService.i.uploadData(url: urls.signedFileUrl, data: fileData)
try await ApiService.i.doAnotherAsyncNetworkCall()
} catch {
longRunningWorker?.deleteFilesIfNecessary()
throw error
}
}
...
Then at some point I will call task.cancel()
.
Whose responsible for cancelling what? Examples I’ve seen so far would use try Task.checkCancellation()
, but for this code that line should appear every few lines – is that how it should be done?
If API service uses URLSession the calls will be cancelled on iOS 15, but we don’t use async variant of URLSession code so we have to cancel the calls manually. Also this applies to all the long running worker code.
I am also thinking that I could add this check within each of async functions, but then basically all async functions would have the same boilerplate code which again seems wrong and I haven’t seen that done in any of the videos.
EDIT:
I have removed callback calls as those are irrelevant to the question.
3
Answers
I'd like to answer my own question with 10 extra months of experience with async/await.
If I were to write such function today, I wouldn't check for cancellation within this task block. I'd call this "manager" task and all the functions it relies on I would call "worker" tasks, as those do the actual long running work.
All of the "worker" functions should check for cancellation before doing work. If any of those functions notices that task is cancelled, then it should throw cancellation error essentially terminating parent task (unless caller uses try/catch block and requires fallback when error is thrown).
I would only explicitly check for cancellation in "manager" task only if completing the "worker" task post-cancellation could somehow corrupt the state.
Basically yes. Cancellation is a totally voluntary venture. The runtime doesn’t know what cancellation means for your particular task, so it just leaves it up to you. You look at
Task.isCancelled
, or, if your intention is tothrow
just in case the task is cancelled, you can callTask.checkCancellation
.Note that if, within your task, you are calling (with
try
) any async material that throws when cancelled, you do not need to any cancellation work with regard to that material, because when it throws due to cancellation, you will throw due to cancellation automatically.Having said all that, I have to add, as a footnote, that your code is extremely strange. Callbacks and async/await are opposites; the idea that you would do a
do/catch
and call a callback within a Task is extremely weird and I would advise against it. You are basically negating all the advantages of a Task by doing that, as well as making untrue the thing I just said about thethrow
trickling up and out of your task.There are two basic patterns for the implementation of our own cancelation logic:
Use
withTaskCancellationHandler(operation:onCancel:)
to wrap your cancelable asynchronous process.This is useful when calling a cancelable legacy API and wrapping it in a
Task
. This way, canceling a task can proactively stop the asynchronous process in your legacy API, rather than waiting until you reach a manualisCancelled
orcheckCancellation
call. This pattern works well with iOS 13/14URLSession
API, or any asynchronous API that offers a cancelation method.Periodically check
isCancelled
ortry
checkCancellation
.This is useful in scenarios where you are performing some manual, computationally intensive process with a loop.
Many discussions about handling cooperative cancelation tend to dwell on these methods, but when dealing with legacy cancelable API, the aforementioned
withTaskCancellationHandler
is generally the better solution.So, I would personally focus on implementing cooperative cancelation in your methods that wrap some legacy asynchronous process. And generally the cancelation logic will percolate up, frequently not requiring additional checking further up in the call chain, often handled by whatever error handling logic you might already have.