Home > Software design >  How to wait for a bunch of async calls to finish to return a result?
How to wait for a bunch of async calls to finish to return a result?

Time:07-12

I understand the basic usage of async/await but I'm a bit confused of what I should do in this specific example. I have an async function called save(url: URL) which mimics a function that would take a local URL as its parameter, and asynchronously return a String which would be, say the new remote URL of this file:

struct FileSaver {
    // In this example I'll simulate a network request
    // with a random async time and return the original file URL
    static func save(_ url: URL) async throws -> String {
        try await Task.sleep(
            seconds: Double(arc4random_uniform(10)) / 10
        )
        return url.absoluteString 
    }
}

extension Task where Success == Never, Failure == Never {
    public static func sleep(
        seconds: Double
    ) async throws {
        return try await sleep(
            nanoseconds: UInt64(seconds) * 1_000_000_000
        )
    }
}

Now say I have 4 local files, and I want to save these files in parallel, but only return when they are all done saving. I read the documentation but still I'm a bit confused if I should use an array or a TaskGroup.

I would like to do something like this:

// in FileSaver
static func save(
    files: [URL]
) async throws -> [String] {
    // how to call save(url) for each file in `files` 
    // in parallel and only return when every file is saved?
}

Thank you for your help

CodePudding user response:

I think withThrowingTaskGroup is what you are looking for:

static func save(
  files: [URL]
) async throws -> [String] {
  try await withThrowingTaskGroup(of: String.self) { group in

    for file in files {
      group.addTask { try await save(file) }
    }

    var strings = [String]()

    for try await string in group {
      strings.append(string)
    }

    return strings
  }
}

CodePudding user response:

We use task group to perform the requests in parallel and then await the whole group.

The trick, though, is that they will not finish in the same order that we started them. So, if you want to preserve the order of the results, we can return every result as a tuple of the input (the URL) and the output (the string). We then collect the group result into a dictionary, and the map the results back to the original order:

static func save(files: [URL]) async throws -> [String] {
    try await withThrowingTaskGroup(of: (URL, String).self) { group in
        for file in files {
            group.addTask { (file, try await save(file)) }
        }

        let dictionary = try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
        return files.compactMap { dictionary[$0] }
    }
}

There are other techniques to preserve the order of the results, but hopefully this illustrates the basic idea.

  • Related