What is the idiomatic way to running two async methods in parallel?

I have two situations that I’m not quite sure how to best write using the new Swift concurrency features. It’s in a Vapor code base, but this isn’t really tied to Vapor.

I’m not quite sure whether a TaskGroup is the right thing to do or async let. Or is there some other way that I’m missing here? What is the best and/or idiomatic way to do this?

The two situations are:

1. An async method that kicks off two other async calls and then returns when both are done

Here’s what I came up with for the two versions:

func runTwoThingsUsingTaskGroup() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        group.addTask { try await self.work1() }
        group.addTask { try await self.work2() }
        try await group.waitForAll() // If any of the two parallel tasks throws an error, the whole method throws
    }
}

func runTwoThingsUsingAsyncLet() async throws {
    async let result1 = self.work1()
    async let result2 = self.work2()
    (_, _) = try await (result1, result2) // If any of the two parallel tasks throws an error, the whole method throws
}

2. An async method that kicks off two other async calls and then returns the result of one of them

Old code

I think thought it’s possible to use a TaskGroup for this, but it’s… not pretty. Or I’m missing something here:

Edit: This doesn’t compile because: Mutation of captured var 'finalResult' in concurrently-executing code

func runTwoThingsUsingTaskGroupWithResult() async throws -> Value {
    return try await withThrowingTaskGroup(of: Void.self, returning: Value.self) { group in
        var finalResult: Value?
        group.addTask { finalResult = try await self.work1() }
        group.addTask { try await self.work2() }
        try await group.waitForAll() // If any of the two parallel tasks throws an error, the whole method throws
        return finalResult!
    }
}

Here’s what I came up with in a async let variant:

func runTwoThingsUsingAsyncLetWithResult() async throws -> Value {
    async let result1 = self.work1()
    async let result2 = self.work2()
    let (finalResult, _) = try await (result1, result2).0 // If any of the two parallel tasks throws an error, the whole method throws
    return finalResult
}

Use async let.

I found @kavon's WWDC session helpful in explaining the difference: basically, async let is for when you have a fixed number of tasks to run concurrently (in this case, 2 tasks).

TaskGroup is for when the number of tasks is only known at runtime (e.g. concurrently processing each element in an Array).

You need to use a TaskGroup, but that's only because we don't expose any real "Future"-like APIs on async let variables. What you want to do is grab the first element from the TaskGroup, and then cancel the other pending tasks (in this case just one other task).

EDIT: Or wait... maybe not. I was under the assumption you want to return the first task which completes without waiting for the other one. If you're happy to wait for both tasks, async let is fine.

3 Likes

Thanks for the answer and the link! One more reason why this solution is more appealing.

Just out of curiosity: Isn’t the “first element of the TaskGroup” more or less random? I understood that when iterating over a TaskGroup, the results are returned in the order they finish. But I want the result of a specific method, the result of the task that returns first.

OK, now we’re on the same page. Thanks for confirming :grinning:

1 Like
Terms of Service

Privacy Policy

Cookie Policy