lukecsmith.co.uk

Comparing Async Let to TaskGroup

Luke, 28 Jul 2024

Heres a standard use case for some asynchronous tasks: you have two tasks to perform, each in the background, and they should start at the same time. You dont know which one will finish first, so you should be able to handle waiting for both to finish, then doing something with the results from both, regardless of whichever finished first.

Theres many ways to achieve this result, but heres two of the more modern solutions using Async Let, versus TaskGroup, both of which were released in Swift 5.5.

First, Async Let. As the name suggests, it allows you to create a property with the result of an async call. At the point when the asynchronous action has completed, the result is available to be evaluated. For example:

async let data1 = fetchResource1()
async let data2 = fetchResource2()

Worth noting that theres no need for the await keyword there.

So for this use case, I need to take both results and use them, regardless of which finished first. Putting the whole thing into a function, its as simple as:

func fetchData() async throws -> (Data, Data) {
    async let data1 = fetchResource1()
    async let data2 = fetchResource2()
    
    return try await (data1, data2)
}

Heres another example, this time creating a further property combining the results of the first set of calls:

func printUserDetails() async {
    async let username = getUser()
    async let scores = getHighScores()
    async let friends = getFriends()

    let user = await UserData(name: username, friends: friends, highScores: scores)
    print("Hello, my name is \(user.name), and I have \(user.friends.count) friends!")
}

How does TaskGroup differ? The biggest difference to the way they work, is that for TaskGroup, the number of operations to be performed can vary at runtime. I could line up any undetermined number of async operations, to be performed as a group, with the results of all the operations being used - where above, I had specified a fixed number of operations in the code. One limitation of TaskGroup against the above though, is that each operation must return the same type.

When you know you only have that fixed number of operations to perform, then Async Let is the way to go. But if not, heres how to do it with TaskGroup:

func fetchDataWithTaskGroup() async throws -> (Data, Data) {
    var results: [Data] = []
    
    try await withTaskGroup(of: Data.self) { group in
        group.addTask {
            return try await fetchResource1()
        }
        group.addTask {
            return try await fetchResource2()
        }
        
        for await result in group {
            results.append(result)
        }
    }
    
    return (results[0], results[1]) // Assuming you know the order
}

This is a fair bit more verbose. You can see that the TaskGroup is populated by using group.addTask. Then the results are appended to an array of Data objects. So as mentioned, you could have an unlimited number of operations here, and that number could be determined at runtime. But importantly, each of those operations in the TaskGroup must return the same type.

For most use cases, Async Let will probably cover it, but its worth remembering that advantage that TaskGroups have for that particular use case.

Tagged with: