According to the proposal, the following code would have waited 3 seconds time(mainFunc) == max(time(awaitOne), time(awaitTwo)) in mainFunc() before returning:
The proposal, probably, a bit misguiding with emphasis on "always 3 seconds" while there is a cancellation part, that can bring a different behaviour in. Your code doesn't wait 3 seconds because Task.sleep has cancellation support, and as proposal states, once scope of the function is left, all child tasks being cancelled first, then awaited – that is clear from the output you see where second task ends with an error. So your second task is simply being cancelled and task finishes early, respecting that. Note that it is still being awaited to complete and process an error, which is main purpose of that implicit awaiting part from the proposal. Without implicit await, you might not see an error as well.
Cancellation on leaving scope of the function aimed to help you prevent (if cancellation handled correctly) cases of jobs you may forgot about:
func someLongTask() async {
while true {
if Task.isCancelled {
return
}
}
}
func main() async {
async let _ = someLongTask()
}
If awaitTwo implementation won't throw an error on cancellation (roughly, using sleep freestanding function, which is not recommended in general), it will indeed wait for it for 3 seconds.
@vns Thank you for the help! That makes sense. I read about the task would cancel the task first, then await. But I was so confused because I don't have enough understanding on Task cancellation, I wasn't able to think of a situation where a task is cancelled but then it is still running.
But after reading more, I guess one can explicitly capture cancel state in the task, but still keep the program running regardless? Or with structure concurrency, cancelling a "parent" task technically needs to cancel all of its children tasks first. So that could also take a while and it is not gonna be an immediate cancellation. Is that correct? Or is there any very frequent use case that just don't support cancel at all?
I actually tried Thread.sleep and that indeed waited for 3s. But I understand that Concurrency doesn't work very well with Thread specific operations / dispatch queues in general. So I thought that's just an edge case.
It does feel like the proposal's statement is kinda misleading. Especially:
The duration of the go2() call remains the same, it is always time(go2) == max(time(f), time(s)) .
At least to me, this sounds like it will wait for the longer function regardless of what happened.
Yes, as cancellation is cooperative here, it’s the developers’ responsibility to property check for it and handle. So in case async function does not handle it or maybe already passed the point where cancellation was possible, or opposite — haven’t reached cancellation point yet.
As for immediate, cancellation almost never has been immediate. I mean, you will get actual task status in Task.isCancelled/Task.checkForCancellation, and withTaskCancellationHandler will allow to catch cancellation as soon as it has been invoked. Still that does not guarantee that work is stopped at this point right away.
In general, you better support cancellation whenever it is possible, especially with long-running and heavy tasks, so it is possible to avoid unnecessary work. Of course, it is still possible that some API doesn't provide proper cancellation. So I think not supporting cancellation should be rare case when you know this work should be completed.
More frequently, I guess, there will be the case when cancellation has been checked only at some points, so task will be executed until certain point after it has been cancelled:
You can expect it to run untill the first cancellation check at this case. That means if, for example, setup and doSomeWork take, say, 10 seconds, the code will finish execution in 10s.
Probably a bit as it does not account for case with cancellation in overall discussion, yet the quoted equation is actually correct, time is variable property, e.g. if function ends earlier due to some exception, it will run less, or if it has conditional path in its body, and so on. So if you think of the time function in this equation as some non-determined function, output of which you cannot just imply from your code (which is true most of the time, as you'll rarely will have sleep in your code), it is pretty accurately describes what to expect.