Concurrency in iOS: serial and concurrent queues in Grand Central Dispatch (GCD) with Swift 4

Today, I’ll show you how to use Swift 4 and the Grand Central Dispatch (GCD) application programming interface (API) to implement the execution of (multiple) tasks in the background, i.e., parallel/concurrent execution of tasks on a multicore CPU. I’ve built a sample app that gives you two options: 1) synchronous execution of tasks in the background and 2) asynchronous execution of tasks in the background. All my Swift 4 code from this article, part of an Xcode 9 project which builds a fully-functional working sample app, is available for download here. Join me in: reviewing concurrent programming concepts; reviewing my concurrent Swift 4 code; and, examining videos of my app in action, videos of console output from my app, and the console output text itself. I’ll even show you how to graphically visualize my app’s CPU and thread usage with Xcode’s Debug Navigator.

This is a look at the app — a snapshot — after all images have finished downloading asynchronously in the background:

Here’s a video of the app downloading images asynchronously in the background:

Press the play button if you missed the first showing


While the code I’ll show you today is written in Swift 4, it was originally written in Swift 3. This article is a revised edition of two Swift 3-based articles I wrote in in 2017, “Concurrency in iOS — Grand Central Dispatch (GCD) with Swift 3” and “Concurrency in iOS: serial and concurrent queues in Grand Central Dispatch (GCD) with Swift 3”, and an Xcode 8.3.1 project stored in a GitHub repo. So I do want to spend some time today discussing the process of upgrading an Xcode project targeting Swift 3 to one targeting Swift 4.

If you need a refresher on concurrency and GCD, you can refer back to my original two 2017 articles here and here.

Introduction

We now live in the day and age of writing apps that can run on devices with CPUs that have multiple cores. We can go way beyond the notion of “multitasking” as a bunch of processes vying for a “timeslice” of the CPU. We can literally run processes simultaneously on different cores. As iOS developers, it is vitally important that we understand the concept of concurrency. Why?

From the perspective of a user, it is easy to understand: An app’s user interface (UI) should always be responsive, no matter how many things the app is doing — and doing simultaneously, like downloading a file and performing geo-location; like a UITableViewCell's thumbnail images loading on demand as a user scrolls up and down through the UITableView.

Concurrency

Let’s briefly refresh our memories. According to Apple, “Concurrency is the notion of multiple things happening at the same time.” Instead of going through all-things-concurrency again, I’m going to provide you with a list of links to concepts and terms that you can easily review:

Grand Central Dispatch (GCD)

Apple’s Grand Central Dispatch (GCD) is probably still the most popular means of tackling this crucial iOS topic of providing concurrency — especially UI responsiveness in apps. While GCD is nice, it’s a bit on the low-level-API end of the spectrum, and hard to fine-tune (like it’s hard to cancel a background task). I encourage you to take a look at my article introducing the abstract Operation class, specifically the BlockOperation class to achieve what I’m accomplishing herein with GCD, but with code that I feel reads as semantically cleaner. But GCD is extremely powerful and still ubiquitous, so I do want you to know about it — and keep in mind that Operation was built on the shoulders of GCD (AH, HA!).

The code

Upgrading my project from Swift 3 to Swift 4
Look through my source code for annotations marked [*0], [*1], [*2], [*3], and [*4] for inline commentary explaining the results of the automatic Swift language upgrade tool I used when I moved from Xcode 8.3.1 to Xcode 9, and thus from Swift 3 to Swift 4. The changes weren’t too drastic but, for example, look at [*1] and [*3].

Consider the statement that the Swift 4 compiler now flags as an error: “UIView.viewWithTag(_:) must be used from main thread only.” I was (past tense; my Swift 3 version) calling it from a background thread:

The compiler was (is) smart enough to detect the fact that I was making this call inside a GCD block that would not be executed on the main thread. It is now smart enough with Swift 4. It wasn’t in Swift 3.

Since I was “just” getting a reference to a UIImageView instance (object), a read-only operation, I assumed it was “safe” to do in a background thread.

Eh, not so fast thar, partner. We’ins gonna git you fer makin’ potentially unsafe calls. (That’s supposed to be the Swift 4 compiler talking. Shhhrrriiiiinnnggggg!)

Doing things concurrently involves a stochastic (random) element. What if another thread/block/task accessed that UIImageView object at the same time I accessed it in my code shown below — like two threads trying to set the UIImageView.image property at the same time? What if iOS did something to that UIImageView object? These scenarios are unlikely because of the way I wrote my code. Each of my download tasks handles one UIImage object.

My gut instinct tells me that the Swift 4 compiler will flag all statements involving the UI (UIKit) from a background thread, even just a reference.

I like it. Better safe than sorry.

Listen to your father! “Never update the UI from a background thread; always use the main thread.” I’ll have more to say on all this Swift version transition/upgrade process soon…

Use of serial and concurrent queues
You can advantageously choose between serial and concurrent GCD queues. If you have a set of tasks, for example a bunch of image downloads that you need to run in the background, but don’t care about the order in which the downloads occur, use a concurrent queue (see function startAsyncImageDownload(_) below). If you have a set of CPU-intensive tasks that you’d like to run in the background, but must be run in a specific order, use a serial queue (see function startSyncImageDownload(_) below). Since a serial queue executes tasks one at a time in the order you specify, they’re great for protecting shared resources; no two or more tasks can access that resource at the same time. Serial queues are also great for tasks that have interdependencies and must be run in a specific order. Think of building a house. You can’t put up the roof until the house’s foundation has been laid and at least one floor with walls has been constructed.


Asynchronous execution – console output
Note that the download image tasks are queued for background execution almost instantly and in order from imageURLs array index 0 to 10 (11 items). The images download asynchronously — the ordering of image download tasks is always stochastic (random) when using a concurrent queue.

Watch the console output from asynchronous task execution:

Press the play button if you missed the first showing

Synchronous execution – console output
Note again that the download image tasks are queued for background execution almost instantly and in order from imageURLs array index 0 to 10 (11 items). While a serial queue executes its tasks one at a time, in the order submitted, it’s still running in the background. It may be slower than a concurrent queue, but it’s still in the background. My rough calculations show that the serial queue download takes, on average, 21% longer than the concurrent queue download.

Watch the console output from synchronous task execution:

Press the play button if you missed the first showing

Good tool – Debug Navigator -> CPU

Worker threads being created
If you bring up Xcode’s Debug Navigator and click on CPU, you can see a visual representation of how hard (or easy) your app is pushing the target device’s CPU. You can see your main thread (1) and count how many other threads iOS has decided are necessary for your app to perform well. Note that as soon as I submitted my 11 download image tasks to dispatch queues, the number of threads jumped almost immediately. The number of threads does not necessarily equal the number of tasks I submit. It’s up to iOS to optimize the details.

Press the play button if you missed the first showing

Worker thread cleanup
If you’re patient and watch the threading details until after the images are downloaded, you’ll see the extra threads get cleaned up. Be patient. It may take awhile for iOS to self-regulate — or the threads may be cleaned up quickly. It’s hard to tell. iOS is pretty smart.

Press the play button if you missed the first showing

Wrapping up

After reviewing everything I’ve presented, you should have a good grasp of concurrency and implementing it through the very popular GCD API. I can’t stress enough how very important it is for you to keep your app’s UI responsive all the time. We live in a very crowded app space. There a millions of smartphone apps out there for consumers to choose from — and choose they will, voting with their feet, if some app they download is sluggish.

I encourage you to broaden your concurrency horizons and look at APIs like Operation which I’ve introduced here. It takes more time to learn, but it is a much more feature-full and semantically abstract technology.

Enjoy!

Author: Andrew Jaffee

Avid and well-published author, software engineer, designer, and developer, now specializing in iOS mobile app development in Objective-C and Swift, but with a strong background in C#, C++, .NET, JavaScript, HTML, CSS, jQuery, SQL Server, MySQL, Oracle, Agile, Test Driven Development, Git, Continuous Integration, Responsive Web Design, blah, blah, blah ... Did I miss any fad-based catch phrases? My brain avatar was kindly provided by http://icons8.com under a Creative Commons Attribution-NoDerivs 3.0 Unported license.

Leave a Reply

Your email address will not be published. Required fields are marked *