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
Save for later
About this episode
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 12. TaskGroup Next episode: 14. Actor

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.

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
}