Why the following code doesn't print "cancelled". Am I checking task cancellation in a wrong way?
import UIKit
class ViewController: UIViewController {
private var task: Task<Void, Never>?
override func viewDidLoad() {
let task = Task {
do {
try await test()
} catch {
if Task.isCancelled {
print("cancelled in catch block..")
}
if let cancellationError = error as? CancellationError {
print("Task canceled..")
}
}
}
self.task = task
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
task.cancel()
}
}
func test() async throws {
while true {
if Task.isCancelled {
print("cancelled..")
throw URLError(.badURL)
}
// OUTPUT:
// "cancelled.." will not be printed
// "Task canceled.." will not be printed
// "cancelled in catch block.." will not be printed
}
}
}
However, if I put if Task.isCancelled { print("cancelled in catch block..") } inside the catch block, cancelled in catch block.. will be executed as expected.
The most significant problem here is that the view controller is isolated to
@MainActor, sotestis isolated to the main actor, too. But this function proceeds to spin quickly, with no suspension points (i.e., noawait), so you are indefinitely blocking the main actor. Therefore, you are blocking the main thread, too, and thus never even reaching yourtask.cancel()line.To solve this, you can either:
Move
testoff the main actor:A
nonisolatedmethod that isasyncwill not run on the current actor. See SE-0338.(Note, getting it off the main actor is, at best, only a partial solution, as you really should never block any of the threads in the Swift concurrency cooperative thread pool. But more on that later.)
Or,
Use the non-blocking
Task.sleepin your loop (which introduces anawaitsuspension point) instead of a tightly spinning loop. This would avoiding the blocking of the main actor, and transform it to something that just periodically checks for cancelation:Actually, because
Task.sleepactually already checks for cancellation, you don’t needcheckCancellationor anisCancelledtest, at all:Never have long-running synchronous code (like your
whileloop) in Swift concurrency. We have a contract with Swift concurrency to never block the current actor (especially the main actor).See SE-0296, which says:
Or see WWDC 2021 video Swift concurrency: Behind the scenes
Bottom line, if
testis really is a time-consuming spinning on the CPU like your example, you would:try Task.checkCancellation(); and alsotry Task.yield().But I must acknowledge the possibility that this
whileloop was introduced when preparing your example (because it really is a bit of an edge-case). If, for example,testis really just calling someasyncfunction that handles cancelation (e.g., a network call), then none of this silliness of manually checking for cancelation is generally needed. It would not block the main actor, and likely already handles cancelation. We would need to see whattestreally is doing to advise further.Setting aside the idiosyncrasies of the code snippet, in answer to your original question, “How to check if the current task is cancelled?”, there are four basic approaches:
Call
async throwsfunctions that already support cancellation. Most of Apple’sasync throwsfunctions natively support cancellation, e.g.,Task.sleep,URLSessionfunctions, etc. And if writing your ownasyncfunction, use any of the techniques outlined in the following three points.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 manualcheckCancellationcall.When we are performing some manual, computationally-intensive process with a loop, we would just
try Task.checkCancellation(). But all the previously mention caveats about not blocking threads, yielding, etc., still apply.Alternatively, you can test
Task.isCancelled, and if so, manually throwCancellationError. This is the most cumbersome approach, but it works.Many discussions about handling cooperative cancelation tend to dwell on these latter two approaches, but when dealing with legacy cancelable API, the aforementioned
withTaskCancellationHandleris generally the better solution.