mikesol
mikesol

Reputation: 1197

Parallel async execution in swift loop

I have swift code that looks like this:

var jsFriendlyFreinds = [JSObject]()
for friend in friends {
  let jsFriend = await FriendsPlugin.createFriendResult(friend)
  jsFriendlyFreinds.append(jsFriend)
}

I'd like for all the async calls to happen at the same time, like how javascript's Promise.all works. For one-offs I know you can use async let, but I'm not sure how to do this in the context of a loop or map. Thanks!

Upvotes: 2

Views: 1358

Answers (3)

Rob
Rob

Reputation: 438437

The basic idea for parallel loop is to use withTaskGroup, and then addTask for each asynchronous call. The trick, though, is that one generally wants to be able to associate the responses with items in the original array (because when running concurrently, you have no assurances as to what order the responses are received). So, you might use a dictionary with the results. E.g., if your Friend object was Identifiable, you might do:

func objects(for friends: [Friend]) async -> [Friend.ID: JSObject] {
    await withTaskGroup(of: (Friend.ID, JSObject).self) { group in
        for friend in friends {
            group.addTask { await (friend.id, FriendsPlugin.createFriendResult(friend)) }
        }

        // build dictionary from array of tuples

        var dictionary: [Friend.ID: JSObject] = [:]
        while let (index, object) = await group.next() {
            dictionary[index] = object
        }

        // now return array of objects in the order that the friends appeared in the original array

        return dictionary
    }
}

Or, more concisely:

func objects(for friends: [Friend]) async -> [Friend.ID: JSObject] {
    await withTaskGroup(of: (Friend.ID, JSObject).self) { group in
        for friend in friends {
            group.addTask { await (friend.id, FriendsPlugin.createFriendResult(friend)) }
        }

        return await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
    }
}

Alternatively, if you would simply like a [JSObject] array:

func objects(for friends: [Friend]) async -> [JSObject] {
    await withTaskGroup(of: (Int, JSObject).self) { group in
        for (index, friend) in friends.enumerated() {
            group.addTask { await (index, FriendsPlugin.createFriendResult(friend)) }
        }

        // build dictionary from array of tuples

        var dictionary: [Int: JSObject] = [:]
        while let (index, object) = await group.next() {
            dictionary[index] = object
        }

        // now return array of objects in the order that the friends appeared in the original array

        return friends.indices.compactMap { dictionary[$0] }
    }
}

Or, again, more concisely:

func objects(for friends: [Friend]) async -> [JSObject] {
    await withTaskGroup(of: (Int, JSObject).self) { group in
        for (index, friend) in friends.enumerated() {
            group.addTask { await (index, FriendsPlugin.createFriendResult(friend)) }
        }

        let dictionary: [Int: JSObject] = await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
        return friends.indices.compactMap { dictionary[$0] }
    }
}

There are many variations on the theme, but the idea is to use withTaskGroup/addTask to run the requests concurrently and then collate the results into some structure by which you can associate the responses with the items in the original array.

Upvotes: 2

You could try this approach, using await withTaskGroup(...), as shown in this example code, to make all the async calls happen in parallel.

let friends: [String] = ["friend-1","friend-2","friend-3"]
var jsFriendlyFreinds = [GoodFriend]()

func getFriends() async {
    // get all friends in parallel
    return await withTaskGroup(of: GoodFriend.self) { group -> Void in
        for friend in friends {
            group.addTask { await createFriendResult(friend) }
        }
        for await value in group {
            jsFriendlyFreinds.append(value)
        }
    }
}

// for testing
func createFriendResult(_ friend: String) async -> GoodFriend {
    // ....
    return GoodFriend(name: "xxx")
}

struct GoodFriend {
   var name: String
   // ....
}

Use it like this:

await getFriends()

Upvotes: 1

Wolfgang Wilke
Wolfgang Wilke

Reputation: 557

You can use the async keyword in your for statement, like

for friend in friends async throws {

Upvotes: 0

Related Questions