I am new to Swift Concurrency and trying to understand Task -> ChildTask relationship.
I created here two Tasks
class MyViewController: ViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("1 \(Thread.current.isMainThread)")
Task {
async let _ = ViewModel().test() // Throws 3.3 CancellationError()
// await ViewModel().test() // Does not cancel task and works fine
print("2 \(Thread.current.isMainThread)")
Task {
do {
Thread.sleep(forTimeInterval: 0.5)
print("4.1 \(Thread.current.isMainThread)")
} catch {
print("4.2 \(Thread.current.isMainThread)")
print(error)
}
}
}
}
actor ViewModel {
func test() async {
print("3.1 \(Thread.current.isMainThread)")
do {
print("3.2 \(Thread.current.isMainThread)")
let images = try await downloadImage()
} catch {
print("3.3 \(error)")
}
}
func downloadImage() async throws -> UIImage {
try Task.checkCancellation()
await Thread.sleep(forTimeInterval: 1) // 1seconds
return UIImage()
}
}
}
async let _ = ViewModel().test() // Throws 3.3 CancellationError()
// await ViewModel().test()
1 true
2 true
3.1 false
3.2 false
3.3 CancellationError()
4.1 true
// async let _ = ViewModel().test()
await ViewModel().test() // Does not cancel task and works
1 true
3.1 false
3.2 false
2 true
4.1 true
My Question here is, why is the Task is cancelled when I dont wait for the async_let test() method
You should not use Thread.current
or Thread.sleep
in async
contexts. This will be an error in Swift 6. Let's remove those and just consider this code:
Task {
async let _ = ViewModel().test()
Task {
do {
try await Task.sleep(for: .milliseconds(500))
} catch {
print(error)
}
}
}
actor ViewModel {
func test() async {
do {
let images = try await downloadImage()
print("Finished!")
} catch {
print("Error: \(error)")
}
}
func downloadImage() async throws -> UIImage {
try Task.checkCancellation()
try await Task.sleep(for: .seconds(1))
return UIImage()
}
}
You seem to think that the task that waits for 0.5 seconds is a "child" of the task that runs ViewModel().test()
. This is not true. By using Task { ... }
, you start a top level task that is not a child of anything. And you don't await
this task, so all the outer Task
does, is start the task of ViewModel().test()
, start another top level task (doesn't wait for it), and ends.
To actually wait for the top-level task, you can do:
await Task {
...
}.value
But if all you want is to wait for some time, you don't need a top-level task at all.Just directly write:
do {
try await Task.sleep(for: .milliseconds(500))
} catch {
print(error)
}
Now the task of "Task.sleep
" is a child task of the single top level task you created. I'll assume you made the above change from now on.
Unlike await
which actually waits, async let
runs in parallel with the code around it. See this section in the Swift guide for an example. No one is waiting for it to complete if you don't await
the variable created by let
(which you didn't even a give a name to) at some point.
async let x = someAsyncThing()
anotherThing() // this line will not wait for someAsyncThing to finish
let result = await x // now we wait
So no one waits for ViewModel().test()
to complete. You only wait for 0.5 seconds after launching ViewModel().test()
in parallel, and that's not enough time for ViewModel().test()
to finish. After 0.5 seconds, the top level task ends, the task running ViewModel().test()
gets cancelled because it is a child task of the top level task. That explains the CancellationError
.