1

I am a bit confused about tasks being cancelled.

Overview:

  • checkCancellation function has 2 child tasks, one task runs computeA and the other computeB. They run concurrently, computeB throws an error.

Doubt:

  • 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?
  • Or is this a bug?

Note:

  • I am using a SwiftUI project (as Swift Playgrounds don't support async let)
  • macOS Big Sur 11.5.2 (20G95)
  • Xcode Version 13.0 beta 5 (13A5212g)

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:

  • When I change the code to let c = try await b + a

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

George
  • 25,988
  • 10
  • 79
  • 133
user1046037
  • 16,755
  • 12
  • 92
  • 138
  • It's the tasks responsibility to check for cancellation. Your "sleep" is not "cancellable", meaning it does not check any "cancellation point", thus it will run till completion. Please read [Task Cancellation](https://developer.apple.com/documentation/swift/task). ;) – CouchDeveloper Aug 24 '21 at 07:11
  • Also, note when calling a static Task function, like `Task.isCancelled` you inspect the task where this statement is executed, i.e.: `Task { ...; print(Task.isCancelled) } ` – CouchDeveloper Aug 24 '21 at 07:25
  • @CouchDeveloper I agree it is up to the implementation to check for cancellation and take appropriate action, however my question why `Task.isCancelled = false` in the output even though `computeB` threw an error, ideally it should have made the ongoing child task to be marked as cancelled, which doesn't happen – user1046037 Aug 24 '21 at 13:08
  • @CouchDeveloper According to https://developer.apple.com/videos/play/wwdc2021/10134/ once one of the child task throws an error, it causes the other child tasks to be marked as cancelled. In my case the other child task wasn't marked as cancelled. That is the doubt / question – user1046037 Aug 24 '21 at 13:11

1 Answers1

4

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141