skip to Main Content

I am new to Swift Concurrency and trying to understand Task -> ChildTask relationship.

I created here two Tasks

  • Parent Task :
    • (1)Calls the test() method in Actor from MainThread.
    • (3.2)The Actor calls the test() method in Background Thread…
    • (2)then returns the control to the parent task on the MainThread
  • Child Task:
    • (3)Runs in MainThread
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

2

Answers


  1. Chosen as BEST ANSWER

    As @Sweeper told, The parent task won't wait for the child task to complete, they only share the Task's Context (whether it is to be cancelled or the thread it was started from)

    I added some defer {logs} to the task, and found that the parent task is deallocated prints 5.x before the cancellation. and so the async_let_task is also cancelled...

    class ViewController: UIViewController {
        
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            
            print("1 ")
            Task {
                async let x = ViewModel().test()        // Throws 3.3 CancellationError()
                // await ViewModel().test()             // Does not cancel task and works fine
                
                print("2 ")
                Task {
                    do {
                        print("4.0 ")
                        Thread.sleep(forTimeInterval: 1)
                        print("4.1 ")
                    } catch {
                        print("4.2 ")
                        print(error)
                    }
                    
                    defer {
                        print("4.X")
                    }
                }
                
                defer {
                    print("5.X")
                }
            }
        }
        
        actor ViewModel {
            func test() async {
                print("3.1 ")
                
                do {
                    let images = try await downloadImage()
                    print("3.2 ")
                } catch {
                    print("3.3 (error)")
                }
                
                defer {
                    print("3.X")
                }
            }
            
            func downloadImage() async throws -> UIImage {
                try Task.checkCancellation()
                await Thread.sleep(forTimeInterval: 1) // 1seconds
                return UIImage()
            }
        }
    }
    
    1 
    2 
    5.X
    3.1 
    4.0 
    3.3 CancellationError()
    3.X
    4.1 
    4.X
    

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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search