Modern Concurrency: Beyond the Basics

Oct 20 2022 · Swift 5.5, iOS 15, Xcode 13.4

Part 2: Concurrent Code

13. Using 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: 12. TaskGroup Next episode: 14. Actor

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

In this episode, you’ll do more with the task group you created in the previous episode. You’ll get and process task results, and control the number of tasks running in parallel.

Getting results from a task group

Open ScanModel and locate runAllTasks. Task groups can return a result that conforms to AsyncSequence, so you can use the reduce method of AsyncSequence:

return await group
  .reduce(into: [String]()) { result, string in
    result.append(string)
  }
await withTaskGroup(of: String.self) { [unowned self] group🟩 -> [String]🟥 in 
🟩let scans = 🟥await withTaskGroup(
  of: String.self
) { [unowned self] group -> [String] in 
print(scans)
["1", "0", "2", "3", "4", "5", "6", "7", "9", "10", "8", "11", "13", "12", "15", "14", "16", "17", "18", "19"]

Processing task results inside the closure

Actually, TaskGroup lets you dynamically manage the workload of the group during execution. So instead of returning the group’s result to be used outside the group, you’ll process results inside the group’s closure.

let scans = await withTaskGroup(of: String.self)   // delete let scans = 
  { [unowned self] group -> [String] in  // delete -> [String]
  for number in 0..<total {
    group.addTask {
      await self.worker(number: number)
    }
  }
  return await group  // delete 3 lines
    .reduce(into: [String]()) { result, string in
      result.append(string)
    }
}
print(scans)  // delete this line
for await result in group {
  print("Completed: \(result)")
}
print("Done.")
...
Completed: 13
Completed: 14
Completed: 15
Completed: 17
Completed: 16
Completed: 19
Completed: 18
Done.

Controlling the group flow

Instead of letting the runtime decide how many tasks to execute and when, you’ll tell it to run at most 4 at a time. In runAllTasks(), replace all the code in the group’s closure:

let batchSize = 4
for index in 0..<batchSize {
  group.addTask {
    await self.worker(number: index)
  }
}
var index = batchSize
for await result in group {
  print("Completed: \(result)")
}
for await result in group {
  print("Completed: \(result)")
  🟩
  if index < total {
    group.addTask { [index] in
      await self.worker(number: index)
    }
    index += 1
  }
  🟥
}

Running code after all tasks have completed

After you run a task group, you usually want to do some cleanup, update the UI or do something else. In this project, you should reset some indicators when the scan is over, so they don’t confuse the user.

await MainActor.run {
  completed = 0
  countPerSecond = 0
  scheduled = 0
}