iosswiftasync-awaitconcurrency

Swift async let Task throws Cancellation Exception if not Awaited


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


Solution

  • 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.