How to cancel a Task
How to cancel a Task 관련
Updated for Xcode 15
Swift’s tasks use cooperative cancellation, which means that although we can tell a task to stop work, the task itself is free to completely ignore that instruction and carry on for as long as it wants. This is a feature rather than a bug: if cancelling a task made it stop work immediately, the task might leave your program in an inconsistent state.
There are seven things to know when working with task cancellation:
- You can explicitly cancel a task by calling its
cancel()
method. - Any task can check
Task.isCancelled
to determine whether the task has been cancelled or not. - You can call the
Task.checkCancellation()
method, which will throw aCancellationError
if the task has been cancelled or do nothing otherwise. - Some parts of Foundation automatically check for task cancellation and will throw their own cancellation error even without your input.
- If you’re using
Task.sleep()
to wait for some amount of time to pass, cancelling your task will automatically terminate the sleep and throw aCancellationError
. - If the task is part of a group and any part of the group throws an error, the other tasks will be cancelled and awaited.
- If you have started a task using SwiftUI’s
task()
modifier, that task will automatically be canceled when the view disappears.
We can explore a few of these in code. First, here’s a function that uses a task to fetch some data from a URL, decodes it into an array, then returns the average:
func getAverageTemperature() async {
let fetchTask = Task { () -> Double in
let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
let readings = try JSONDecoder().decode([Double].self, from: data)
let sum = readings.reduce(0, +)
return sum / Double(readings.count)
}
do {
let result = try await fetchTask.value
print("Average temperature: \(result)")
} catch {
print("Failed to get data.")
}
}
await getAverageTemperature()
Now, there is no explicit cancellation in there, but there is implicit cancellation because the URLSession.shared.data(from:)
call will check to see whether its task is still active before continuing. If the task has been cancelled, data(from:)
will automatically throw a URLError
and the rest of the task won’t execute.
However, that implicit check happens before the network call, so it’s unlikely to be an actual cancellation point in practice. As most of our users are likely to be using mobile network connections, the network call is likely to take most of the time of this task, particularly if the user has a poor connection.
So, we could upgrade our task to explicitly check for cancellation after the network request, using Task.checkCancellation()
. This is a static function call because it will always apply to whatever task it’s called inside, and it needs to be called using try
so that it can throw a CancellationError
if the task has been cancelled.
Here’s the new function:
func getAverageTemperature() async {
let fetchTask = Task { () -> Double in
let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
try Task.checkCancellation()
let readings = try JSONDecoder().decode([Double].self, from: data)
let sum = readings.reduce(0, +)
return sum / Double(readings.count)
}
do {
let result = try await fetchTask.value
print("Average temperature: \(result)")
} catch {
print("Failed to get data.")
}
}
await getAverageTemperature()
As you can see, it just takes one call to Task.checkCancellation()
to make sure our task isn’t wasting time calculating data that’s no longer needed.
If you want to handle cancellation yourself – if you need to clean up some resources or perform some other calculations, for example – then instead of calling Task.checkCancellation()
you should check the value of Task.isCancelled
instead. This is a simple Boolean that returns the current cancellation state, which you can then act on however you want.
To demonstrate this, we could rewrite our function a third time so that cancelling the task or failing to fetch data returns an average temperature of 0. This time we’re going to cancel the task ourselves as soon as it’s created, but because we’re always returning a default value we no longer need to handle errors when reading the task’s result:
func getAverageTemperature() async {
let fetchTask = Task { () -> Double in
let url = URL(string: "https://hws.dev/readings.json")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
if Task.isCancelled { return 0 }
let readings = try JSONDecoder().decode([Double].self, from: data)
let sum = readings.reduce(0, +)
return sum / Double(readings.count)
} catch {
return 0
}
}
fetchTask.cancel()
let result = await fetchTask.value
print("Average temperature: \(result)")
}
await getAverageTemperature()
Now we have one implicit cancellation point with the data(from:)
call, and an explicit one with the check on Task.isCancelled
. If either one is triggered, the task will return 0 rather than throw an error.
Tips
You can use both Task.checkCancellation()
and Task.isCancelled
from both synchronous and asynchronous functions. Remember, async functions can call synchronous functions freely, so checking for cancellation can be just as important to avoid doing unnecessary work.