The problem is not cancellation, per se, but timing: f1
passed the checkCancellation
line (and possibly even finished) before you cancelled t1
. f1
is an async
function, but isnāt really doing anything asynchronous within it, so it finishes execution exceptionally quickly. There is a race between t1
being canceled and f1
checking for cancellation.
However, if your function does something that really is asynchronous (and does something that responds to cancellation), then you see the behavior you are looking for. For example, below, f2
takes 10 seconds, but t2
is cancelled after one second, so we can see cancellation in action:
nonisolated func f2() async throws {
try await Task.sleep(for: .seconds(10))
print("inside f2")
}
let t2 = Task {
try await f2()
}
Task {
try await Task.sleep(for: .seconds(1))
t2.cancel()
print("cancelled before `f2` got to `print` statement")
}
FWIW, the withTaskCancellationHandler
solves a very different problem: Itās for propagating cancellation when using unstructured concurrency:
func foo(url: URL) async throws {
let task = Task {ā¦}
return try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
This way, if you cancel foo
, it will cancel the unstructured concurrency within foo
. If you donāt use withTaskCancellationHandler
, the cancellation of foo
would not cancel the unstructured concurrency within.
(As an aside, this is one of the reasons why we generally prefer structured concurrency, because cancellation is propagated for us automatically. But where we really need unstructured concurrency, we always use this withTaskCancellationHandler
pattern, to remedy this lack of automatic cancellation propagation.)
As to your broader question, āHow to cancel an async funcā, there are a ton of different cancellation patterns:
-
The cancelling of the unstructured task with asynchronous work, as outlined at my f2
example, above, is a fine way to do it (with the typical caveats about unstructured concurrency, notably that you have to handle cancellation propagation manually, that the called function supports cancellation, etc.).
-
If you are using SwiftUI, it does some automatic cancellation for you. For example, this will start foo
when the view is presented, but the .task {ā¦}
view modifier will cancel it for you when the view is dismissed:
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {ā¦}
.task {
await foo()
}
}
func foo() async {ā¦}
}
There is no need to manually cancel foo
in this case.
-
If you use task groups, you can cancel the group when one of them fails. Or if you use a discarding task group, it will automatically do this for you:
func foo() async throws {
try await withThrowingDiscardingTaskGroup { group in
group.addTask { try await f3() }
group.addTask { try await f4() }
}
}
In this case, if either f3
or f4
throws an error, the other will be cancelled (if it hasnāt finished already).
-
If you are using async let
, if you let the task fall out of scope without awaiting it, it will be cancelled. For example, this example is adapted from the one in SE-0317:
func go() async {
async let f = fast() // 300ms
async let s = slow() // 3seconds
return "nevermind..."
// implicitly: cancels f
// implicitly: cancels s
// implicitly: await f (giving it a chance to respond to the cancellation)
// implicitly: await s (giving it a chance to respond to the cancellation)
}
So, if you really are just trying to cancel some unstructured concurrency, then option 1 is sufficient. But for the broader question of how to cancel an async
function, as you can see, there are a plethora of alternatives. It boils down to the particular problem that you are trying to solve.
Implicit in all of these examples is that the function you are calling must respond to cancellation. Fortunately, most of the async
functions we call (e.g., Task.sleep
, URLSession
, etc.) already do this for us. As a result, we donāt tend to use Task.checkCancellation
(which checks for cancellation, and if not cancelled, just proceeds to the next line) very often: Its use is largely limited to our own custom compute tasks where we periodically would check for cancellation inside a loop, or what have you. But thatās a bit of an edge case.