18

I want to get data in parallel. I found an example to call API in parallel but I want to store async let variables with loop.

Async let example. However, this example doesn't use a loop.

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

I want to do something like the following.

let items = photoNames.map({ photo in
    async let item = downloadPhoto(named: photo)
    return item
}) 
let photos = await items
show(photos)
2
  • 1
    Does the backend not have an API for downloading multiple photos at once? Commented Nov 14, 2021 at 11:06
  • Backend doesn't have one Commented Nov 14, 2021 at 11:43

3 Answers 3

26

You can use a task group. See Tasks and Task Groups section of the The Swift Programming Language: Concurrency (which would appear to be where you got your example).

One can use withTaskGroup(of:returning:body:) to create a task group to run tasks in parallel, but then collate all the results together at the end.

E.g. here is an example that creates child tasks that return a tuple of “name” and ”image”, and the group returns a combined dictionary of those name strings with their associated image values:

func downloadImages(names: [String]) async -> [String: UIImage] {
    await withTaskGroup(
        of: (String, UIImage).self,
        returning: [String: UIImage].self
    ) { [self] group in
        for name in names {
            group.addTask { await (name, downloadPhoto(named: name)) }
        }

        var images: [String: UIImage] = [:]

        for await result in group {
            images[result.0] = result.1
        }

        return images
    }
}

Or, more concisely:

func downloadImages(names: [String]) async -> [String: UIImage] {
    await withTaskGroup(of: (String, UIImage).self) { [self] group in
        for name in names {
            group.addTask { await (name, downloadPhoto(named: name)) }
        }

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

They run in parallel:

enter image description here

But you can extract them from the dictionary of results:

let stooges = ["moe", "larry", "curly"]
let images = await downloadImages(names: stooges)

imageView1.image = images["moe"]
imageView2.image = images["larry"]
imageView3.image = images["curly"]

Or if you want an array sorted in the original order, just build an array from the dictionary:

func downloadImages(names: [String]) async -> [UIImage] {
    await withTaskGroup(of: (String, UIImage).self) { [self] group in
        for name in names {
            group.addTask { await (name, downloadPhoto(named: name)) }
        }

        let dictionary = await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
        return names.compactMap { dictionary[$0] }
    }
}
Sign up to request clarification or add additional context in comments.

6 Comments

Why use the returning argument?
In this case, the return type can inferred, so it’s not needed.
how would you approach this if you wanted to cancel and individual image fetch? my understanding is it's not easy (maybe even possible) to cancel individual child tasks in a task group? my use case is for multiple concurrent large file uploads
For ultimate control, you’d use unstructured concurrency. It is the tool of last resort (as you lose lots of wonderful structured concurrency benefits such as automatic cancelation propagation, which you have to implement manually yourself with unstructured concurrency), but it is the most powerful and flexible approach.
How do you deal with @MainActor APIs where the API itself must be called from main actor, but the internal of the API uses background thread?
It’s impossible to answer that question on the basis of what you’ve provided. Generally, the actor/queue/thread used internally is immaterial on the actor isolation of the method you are calling. I'd suggest you provide a real-world example, and perhaps just post it as a new question. You can share that link here, but we’re not going to be able to answer the question here in the comments of a loosely-related question…
4

Channels and Heap are the most recent tools, as of the time of this answer update, which provide an order-maintaining solution.

import AsyncAlgorithms

let photos = await Array(photoNames.mapWithTaskGroup(downloadPhoto))
import AsyncAlgorithms
import HeapModule

public extension Sequence where Element: Sendable {
  /// Transform a sequence asynchronously, and potentially in parallel.
  /// - Returns: An `AsyncSequence` which returns transformed elements, in their original order,
  /// as soon as they become available.
  func mapWithTaskGroup<Transformed: Sendable>(
    priority: TaskPriority? = nil,
    _ transform: @escaping @Sendable (Element) async -> Transformed
  ) -> AsyncChannel<Transformed> {
    let channel = AsyncChannel<Transformed>()
    Task { await mapWithTaskGroup(channel: channel, transform) }
    return channel
  }

  /// Transform a sequence asynchronously, and potentially in parallel.
  /// - Returns: An `AsyncSequence` which returns transformed elements, in their original order,
  /// as soon as they become available.
  func mapWithTaskGroup<Transformed: Sendable>(
    priority: TaskPriority? = nil,
    _ transform: @escaping @Sendable (Element) async throws -> Transformed
  ) -> AsyncThrowingChannel<Transformed, Error> {
    let channel = AsyncThrowingChannel<Transformed, Error>()
    Task {
      do {
        try await mapWithTaskGroup(channel: channel, transform)
      } catch {
        channel.fail(error)
      }
    }
    return channel
  }
}

// MARK: - private
private protocol AsyncChannelProtocol<Element> {
  associatedtype Element
  func send(_: Element) async
  func finish()
}

extension AsyncChannel: AsyncChannelProtocol { }
extension AsyncThrowingChannel: AsyncChannelProtocol { }

private extension Sequence where Element: Sendable {
  private func mapWithTaskGroup<Transformed: Sendable>(
    channel: some AsyncChannelProtocol<Transformed>,
    priority: TaskPriority? = nil,
    _ transform: @escaping @Sendable (Element) async throws -> Transformed
  ) async rethrows {
    typealias ChildTaskResult = Heap<Int>.ElementValuePair<Transformed>
    try await withThrowingTaskGroup(of: ChildTaskResult.self) { group in
      for (offset, element) in enumerated() {
        group.addTask(priority: priority) {
          .init(offset, try await transform(element))
        }
      }

      var heap = Heap<ChildTaskResult>()
      var lastSentOffset = -1
      for try await childTaskResult in group {
        heap.insert(childTaskResult)
        // Send as many in-order `Transformed`s as possible.
        while heap.min()?.element == lastSentOffset + 1 {
          await channel.send(heap.removeMin().value)
          lastSentOffset += 1
        }
      }

      channel.finish()
    }
  }
}
import HeapModule

public extension Heap {
  /// A "`Value`" that uses an accompanying `Heap.Element` for sorting  via a `Heap`.
  /// - Note: If `Value` itself is `Comparable`, it can of course be inserted into a Heap directly.
  ///   This type is explicitly for cases where a different sorting rule is desired.
  struct ElementValuePair<Value> {
    public var element: Element
    public var value: Value
  }
}

// MARK: - public
public extension Heap.ElementValuePair {
  init(_ element: Element, _ value: Value) {
    self.init(element: element, value: value)
  }
}

// MARK: - Comparable
extension Heap.ElementValuePair: Comparable {
  public static func < (lhs: Self, rhs: Self) -> Bool {
    lhs.element < rhs.element
  }

  /// Only necessary because Comparable: Equatable. 😞
  public static func == (lhs: Self, rhs: Self) -> Bool {
    fatalError()
  }
}

// MARK: - Sendable
extension Heap.ElementValuePair: Sendable where Element: Sendable, Value: Sendable { }

2 Comments

This needs some explanation. Code-only answers are not very helpful.
Your second extension is using an internal type. I think that would deserve some explanation because it looks like something that can break easily.
-3

If the order of result doesn't matter here, use a TaskGroup instead.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.