user1046037
user1046037

Reputation: 17695

Swift async let cancellation doesn't work

I am a bit confused about tasks being cancelled.

Overview:

Doubt:

Note:

Output:

A - started
B - going to throw
A - going to return, Task.isCancelled = false
error: infinity

Concurrent Function Definitions:

import Foundation
import UIKit

enum ComputationError: Error {
    case infinity
}

fileprivate func computeA() async throws -> Int {
    print("A - started")
    await Task.sleep(2 * 100_000_000)
    print("A - going to return, Task.isCancelled = \(Task.isCancelled)") //I expected Task.isCancelled to be true
    return 25
}

fileprivate func computeB() async throws -> Int {
    print("B - going to throw")
    throw ComputationError.infinity
}

func checkCancellation() async throws {
    async let a = computeA()
    async let b = computeB()
    
    let c = try await a + b
    print("c = \(c)")
}

Invoking Concurrent function

struct ContentView: View {
    var body: some View {
        Button("check cancellation") {
            Task {
                do {
                    try await checkCancellation()
                    print("normal exit")
                } catch {
                    print("error: \(error)")
                }
            }
        }
    }
}

Observation:

Output:

A - started
B - going to throw
A - going to return, Task.isCancelled = true
error: infinity

Doubt:

I am still not sure I understand the reason for this behaviour in the original code

Upvotes: 1

Views: 2196

Answers (1)

matt
matt

Reputation: 535222

I expected child task computeA to be cancelled because computeB threw an error, but computeA was never cancelled. Is my understanding wrong or am I missing something?

Yes and yes. Your understanding is wrong and you are missing something.

The remark in the video has to do with subtasks created in a task group. What actually happens is that when the error percolates from the child task up to the task group, the task group calls cancel on all the other child tasks.

But you are testing using async let. There is no task group. So there is no one "there" to cancel the other subtask. In fact, if you watch the video carefully, you'll find that it tells you correctly what to expect:

  • If the parent task is cancelled, the child tasks are cancelled automatically.

  • If an async let task throws before a second async let task is even initialized, then the second task is "born cancelled". It is still awaited (the video is very clear about this, and it is correct); if it doesn't respond to cancellation by aborting, it will continue to completion.

The point here is that the parent task must end in good order. It cannot end until its children have completed one way or another. (Again, the video is very clear about this.) If that means automatically waiting for the tasks to complete in spite of the throw, then that's what will happen.

Another interesting thing to note is that with async let, where you say try await later, if the subtask throws, you won't even hear about it (and nothing at all will happen in this regard) until you reach the point where the try await takes place. In other words, the throw can only percolate up to the point where you actually say try; nothing else would make sense.

Upvotes: 4

Related Questions