Nicer way to write code that awaits two concurrently executed async functions

Is there a nicer way to write the following?

async let a: () = someAsyncFuncThatReturnsVoid()
async let b: () = someAsyncFuncThatReturnsVoid()
_ = await (a, b)
doSomething()

When doSomething() is executed, both a and b should be done.

AFAICS the only reason I have to name a and b is because I have to await them, and the explicit type () is the result of applying the suggested fix from a warning I would get otherwise.

1 Like

Try using task group instead

2 Likes

Thanks! Like this, or is there a less verbose way?

await withTaskGroup(of: Void.self) {
  $0.addTask { await someAsyncFuncThatReturnsVoid() }
  $0.addTask { await someAsyncFuncThatReturnsVoid() }
}
doSomething()

You can shorten it a bit

func withVoidTaskGroup(body: (inout TaskGroup<Void>) async -> Void) async {
  await withTaskGroup(of: Void.self, body: body)
}

in your call site

await withVoidTaskGroup {
  $0.addTask { await doStuffA() }
  $0.addTask { await doStuffA() }
}

That’s not the best way to “shorten” the task group definition — we’re shipping DiscardingTaskGroup | Apple Developer Documentation for those use cases when you don’t use the results. It’s more efficient in quickly discarding the values.

Though tbh the two async lets are probably the shortest way I guess. And slightly more efficient since async let tasks are allocated in the parent tasks allocator which may not necessarily have to hit the heap.

TLDR. I’d stick to the two async lets.

12 Likes

I too run into this conundrum occasionally, and find the async let approach very boilerplaty. I tend to just use task groups, even if they are less efficient in principle, because the resulting code is much cleaner.

If you don't like having to name the things, you can also use end of scope to wait them out:

        do {
            async let _ = async1()
            async let _ = async2()
        }
        // async1() and async2() will have finished before getting here
8 Likes

doesn't that cancel them?

5 Likes

Good point. Not awaiting the result will cause the task to get cancelled. It will at least still run to completion before we exit the scope.

2 Likes