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've reached locked video content where the transcript will be shown as obfuscated text.

You've used Async lit to run tasks concurrently. But what if you need to run a 1000 tasks in parallel or you don't know until run time how many tasks need to run in parallel? You need more than async lit. The answer is task group. You can create concurrency on the fly and safely process the results while reducing the possibility of data races. Here's how you might use a task group. You set each task's return type as data with the of argument. The group as a whole will return an array of UI image. You could also have an explicit return type in the closure declaration and skip the returning argument. Elsewhere in your code, you've calculated the number of images you want to fetch and you loop through them here. Group is the ready to go throwing task group. Inside the for loop, you use group.adtask to add tasks to the group. You perform the actual work of the task by fetching data from a url. Task groups conform to async sequence. So as each task in the group completes, you collect the results into an array of images and return it. Here's a diagram of what the code does. The example code starts a variable number of concurrent tasks and each one downloads an image. Finally, you assign the array with all the images to images. You manage the group's tasks with the following API. Add task priority operation, add the task to the group for concurrent execution with the given optional priority. Add task unless canceled, priority operation is identical to add task, except that it does nothing if the group is already canceled. Cancel all cancels the group, it cancels all currently running tasks along with all tasks added in the future. Is canceled returns true if the group is canceled? Is empty returns true if the group has completed all its tasks, or has no tasks to begin with. Waitforall waits until all tasks have completed. Use this when you need to execute some code after finishing the group's work. In this episode, you'll work with the Sky App. It pretends to scan satellite images of the sky. Each image is divided into 20 sectors and the app scans each sector looking for signs of alien life. Each sector scan is independent of the other sector scans. So the app could do these concurrently and that's what you'll implement in this episode using task group. Open the starter project, then build and run. The view shows three indicators, number of scheduled tasks, current tasks per second ratio, and number of completed scans. By the end of this episode, the first indicator should be more than one, tap "Engage Systems". The button action should create and run scans for the 20 sectors, then display how long the complete scan took. At the moment, none of the action code is there. You'll write it soon. When you finish this episode, the duration should be much less than 20 seconds. Stop the run, then open scanmodel.swift and see what's here. ScanModel is an observable object. It has several published properties. The app's views use these properties so any updates must happen on the main queue. The main actor keyword places these properties on the main actor. You'll learn more about actors later in this course. Now jump down to the extension. It already has two private utility methods to track task progress. On task completed, updates completed, counted, scheduled, and count per second. And on scheduled updates scheduled. So both of these are main actor methods. Back in the scan model class, you have a convenience method worker number to run a single task. A wait on scheduled updates the scheduled counter. Updating the UI should always be a fast operation. So the await here won't significantly affect the progress of the scanning task. The method creates a new scan task with the given sector number. Waits for the result of the asynchronous call to task.run, and then calls on task completed to update the counters and the apps UI on the main thread. And finally it returns result. So worker number not only runs a single task but also tracks the execution in the model state. Because on task completed and unscheduled are main actor methods, you can safely update the published properties even while running multiple copies of this method in parallel. Now add some code to run all tasks. You create and run a task for every sector, build and run. Watch the indicators when you tap "Engage Systems". This video has been sped up, but in your simulator, there's always exactly one scheduled task and the app processes about one task per second. This is because the loop is awaiting the completion of each task before starting the next. So the tasks run serially, not concurrently. When the full scan finishes, the duration is just over 20 seconds. You know these tasks can run concurrently, so here's how you make that happen. Select all the code you added to run all tasks and replace it. You're creating and running a task group where each task returns a string. Inside the closure, you'll call worker number. So you capture yourself as unowned reference. Inside the task group closure, loop over the sector numbers. Then create and run each task. You add the task for this sector, then move right along to the next loop iteration. This time to see the effects of task group, build and run on a device. It has more CPUs, so more threads than the simulator. Connect your device and take care of any security alerts. In the target editor, signing and capabilities, customize the bundle ID. Check automatically "Manage Signing". And set a team. Select your device. Then build and run. The first time, you might have to trust your app after it installs. Go to "Settings". "General". "VPN and Device Management", "Developer App". "Apple Development". "Trust Apple Development". Now you can run the app. Tap "Engage Systems". Scheduled shoots up to 20 and when the tasks finish, the duration is some fraction of 20. On my iPhone, it's four seconds, which means the app used five threads. But you didn't see many UI updates until all the scan tasks finished, which isn't very useful. You'll fix this now. Here's the problem, by default, a task inherits its parents' 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. One solution is to reduce the priority of the scan tasks. Get back to scantask.swift. In "Run", set the priority of the task. Setting a lower priority for a new scan task suggests the scheduler should favor resuming a completed scan over starting a new one. UI updates are fast, so this won't slow down the total scan much. So instead we're getting stuck behind all the scan tasks, UI update tasks move to the front of the queue, build and run on a simulator. This will probably run on only one thread, so updates will appear slowly enough to watch what's happening. That's better. Now the other two indicators update regularly while the scan tasks run. In the next episode, you'll get and process results from the scan tasks.