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
About this episode

Leave a rating/review

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

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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)