Async let created tasks

In TaskGroup | Apple Developer Documentation it mentions “Structured concurrency APIs (including task groups and async let), always waits for the completion of tasks contained within their scope before returning. Specifically, this means that even if you await a single task result and return it from a withTaskGroup function body, the group automatically waits for all the remaining tasks before returning”

What I am confused about is how is async let the same as a Task Group? Doesn’t async let just create one task so it’s like calling a Task rather than Task Group? It sounds like theyre saying async let can have multiple tasks under its initialization and that it waits for all of the tasks to complete.

To be clear async let initialises a single asynchronous task while TaskGroup allows you to add a dynamic amount of concurrency. So how are they the same? And yes, if I understand you correctly, you can initialise multiple asynchronous tasks using multiple async lets, and then if you were returning a result from awaiting these initialisations, that you’d need to wait for all the tasks to complete before you could return them. There’s also a section in the proposal related to async let, SE-0317, which says “any structured invocation will take as much time to return as the longest of its child tasks takes to complete”, which is another way of saying that all the tasks need to be awaited on before a function containing these initialisations can return.

1 Like

@asaadjaber To be clear, when people talk about the async let feature, do they mean the whole async let + the await at the end or just the async let declaration? I guess I still confused.

Both async let and task groups are structured concurrency, notably enjoying automatic propagation of cancellation. See SE-0317, which says:

Later it says:

You go on to ask:

In contrast to async let, Task {…} is unstructured concurrency, meaning, amongst other things, you do not enjoy automatic cancellation propagation. And if you want to enjoy cancellation propagation with unstructured concurrency, you have to manually do this yourself with, for example, withTaskCancellationHandler.

Furthermore, if you create a Task {…} (unstructured concurrency) and do not explicitly await it, it will continue to run even after it falls out of scope. If you want to cancel it later, you have to save the reference to the Task somewhere and manually cancel it yourself.

With async let, however, you enjoy automatic cancellation propagation. Also, if you let it fall out of scope and neglect to await it, its task will be immediately cancelled (and, coincidentally, implicitly await that task to respond to the cancellation). See SE-0317 – Implicit async let awaiting.

Finally, you asked:

In practice, they’re generally talking about async let that you will eventually await.

There generally is little point in ever doing async let without an explicit await because if it falls out of scope before you await, it will be immediately cancelled (unlike Task {…}), as I discussed above. That having been said, we consciously leverage this at times, e.g., for early-exit scenarios, where this simplifies our code not having to manually cancel tasks that we no longer need.

But, unlike structured concurrency, when you use async let, you always await, generally with an explicit await, and occasionally relying on the implicit cancel/await logic.

1 Like

It is generally the whole async let for creating the structured concurrency task (i.e. initialising an asynchronous task using async let) and the await, which awaits the result of the asynchronous task. However, bear in mind that if you do not await the result of an asynchronous task that you declare using async let, the system implicitly awaits its result as described in SE-0317.

Consider the following analogy: there are three people in a room, and one of them asks the other two for coffee and toast. One person is in charge of getting coffee, one for getting toast, and the third one waits for the other two to get the coffee and toast. The third person won’t be able to eat breakfast until they have their coffee and toast (i.e. the result of awaiting the coffee and toast would be an eatBreakfast operation). If the third person decides to eatBreakfast without awaiting the results of coffee and toast, the system will still await the results of getting coffee and getting toast before the whole operation of having breakfast completes. And awaiting here simply means waiting for the results of getting coffee and toast to return. What’s more, the results of these operations are implicitly cancelled by the system (i.e. their isCancelled property is marked true), while the system awaits their results to be completed.

A TaskGroup on the other hand is like having a basket with an, initially, unknown amount of items in it that are there for making breakfast. The basket could have eggs, toast, coffee, bananas etc. And as you add these items to your task group to prepare breakfast, whose initial amount is unknown, you initialise concurrent tasks. This refers to the “dynamic amount of concurrency” aspect of TaskGroups.

I suppose you are correct in that TaskGroup and async let are the same in that all the tasks that are added to a task group, or initialised via async let need to complete before the result of an operation such as making breakfast, in this example, can return. And there are underlying mechanics like implicit await which is unique to async let and cancellation, which takes place when declaring a property using async let is not awaited, and is implicitly cancelled by the system.

I hope this helps clarify how TaskGroup and async let are different.