Modern Concurrency: Beyond the Basics

Oct 20 2022 Swift 5.5, iOS 15, Xcode 13.4

Part 2: Concurrent Code

12. TaskGroup

Episode complete

Play next episode

Next
Save for later
About this episode
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 11. Introduction Next episode: 13. Using TaskGroup

This video TaskGroup was last updated on Oct 20 2022

Heads up... You've reached locked video content where the transcript will be shown as obfuscated text.

You can unlock the rest of this video course, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.

You’ve used async let to run tasks concurrently, but what if you need to run a thousand tasks in parallel, or you don’t know until runtime how many tasks need to run in parallel? You need more than async let!

//1
let images = try await withThrowingTaskGroup(
  of: Data.self
  returning: [UIImage].self
) { group in
  // 2
  for index in 0..<numberOfImages {
    let url = baseURL.appendingPathComponent("image\(index).png")
    // 3
    group.addTask {
      // 4
      return try await URLSession.shared
        .data(from: url, delegate: nil)
        .0
    }
  }
  // 5
  return try await group.reduce(into: [UIImage]()) { result, data in
    if let image = UIImage(data: data) {
      result.append(image)
    }
  }
}
/// Currently scheduled for execution tasks.
@MainActor @Published var scheduled = 0

/// Completed scan tasks per second.
@MainActor @Published var countPerSecond: Double = 0

/// Completed scan tasks.
@MainActor @Published var completed = 0

@Published var total: Int

@MainActor @Published var isCollaborating = false
extension ScanModel {
  @MainActor
  private func onTaskCompleted() {
    completed += 1
    counted += 1
    scheduled -= 1

    countPerSecond = Double(counted) / Date().timeIntervalSince(started)
  }

  @MainActor
  private func onScheduled() {
    scheduled += 1
  }
}
func worker(number: Int) async -> String {
  await onScheduled()

  let task = ScanTask(input: number)
  let result = await task.run()

  await onTaskCompleted()
  return result
}

Serial tasks

Now, add some code to runAllTasks():

var scans: [String] = []
for number in 0..<total {
  scans.append(try await worker(number: number))
}
print(scans)

Concurrent tasks

You know these tasks can run concurrently, so here’s how you make that happen:

await withTaskGroup(of: String.self) { [unowned self] group in

}
await withTaskGroup(of: String.self) { [unowned self] group in
🟩
  for number in 0..<total {

  }
🟥
}
await withTaskGroup(of: String.self) { [unowned self] group in
  for number in 0..<total {
🟩
    group.addTask {
      await self.worker(number: number)
    }
🟥
  }
}

Updating the UI

Here’s the problem: By default, a task inherits its parent’s priority value, so the scan tasks and their UI updating subtasks all have the same priority, and the UI updates go into the same queue that already contains all the scan tasks. So no UI updates can appear until all the scan tasks have finished.

await Task(priority: .medium)