Grand Central Dispatch Tutorial for Swift 5: Part 1/2
Learn all about multithreading, dispatch queues and concurrency in the first part of this Swift 5 tutorial on Grand Central Dispatch. By Fabrizio Brancati.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Grand Central Dispatch Tutorial for Swift 5: Part 1/2
30 mins
- Getting Started
- Breaking Down GCD Concepts
- Learning Concurrency
- Understanding Queues
- Understanding Queue Types
- Scheduling Synchronous vs. Asynchronous Functions
- Managing Tasks
- Handling Background Tasks
- Delaying Task Execution
- Managing Singletons
- Handling the Readers-Writers Problem
- Reasoning About Dispatch Barriers
- Where to Go From Here?
Grand Central Dispatch (GCD) is a low-level API for managing concurrent operations. It can help improve your app’s responsiveness by deferring computationally expensive tasks to the background. It’s an easier concurrency model to work with than locks and threads.
In this two-part Grand Central Dispatch tutorial, you’ll learn the ins and outs of GCD and its Swifty API. This first part explains what GCD does and showcases several basic GCD functions. In the second part, you’ll learn about some advanced functions GCD has to offer.
You’ll build upon an existing app called GooglyPuff. GooglyPuff is a non-optimized, “thread-unsafe” app that overlays googly eyes on detected faces using Core Image’s face detection API. You can select images on which to apply this effect from your photo library or download images from the internet.
Your mission is to use GCD to optimize the app and ensure you can safely call code from different threads.
async
and await
has arrived, and a massive set of improvements has also landed in the language.
This tutorial will help you to better understand how GCD works with serial and concurrent queues. It’s also beneficial to learn about these new asynchronous features that are now available.
In this Grand Central Dispatch tutorial, you’ll delve into basic GCD concepts, including:
- Multithreading
- Dispatch queues
- Concurrency
Getting Started
Use Download Materials at the top or bottom of this tutorial to download the starter project. Open it in Xcode, and run it to see what you have to work with.
CIDetector
work.The home screen is initially empty. Tap +, then select Le Internet to download predefined images from the internet. Tap the first image, and you’ll see googly eyes added to the face.
You’ll primarily work with four classes in this tutorial:
-
PhotoCollectionViewController
: The initial view controller. It displays the selected photos as thumbnails. -
PhotoDetailViewController
: Displays a selected photo fromPhotoCollectionViewController
and adds googly eyes to the image. -
Photo
: This protocol describes the properties of a photo. It provides an image, a thumbnail and their corresponding statuses. The project includes two classes that implement the protocol:-
DownloadPhoto
, which instantiates a photo from an instance ofURL
. -
AssetPhoto
, which instantiates a photo from an instance ofPHAsset
.
-
-
PhotoManager
: This manages all thePhoto
objects.
The app has a few problems. One that you may have noticed when running it is that the download complete alert is premature. You’ll fix this in the second part of the series.
In this first part, you’ll work on a few improvements, including optimizing the googly-fying process and making PhotoManager
thread-safe.
Breaking Down GCD Concepts
To understand GCD, you need to be comfortable with several concepts related to concurrency and threading.
Learning Concurrency
In iOS, a process or application consists of one or more threads. The operating system scheduler manages the threads independently of each other. Each thread can execute concurrently, but it’s up to the system to decide if, when and how it happens.
Single-core devices achieve concurrency through a method called time-slicing. They run one thread, perform a context switch, then run another thread.
Multi-core devices, on the other hand, execute multiple threads at the same time via parallelism.
GCD is built on top of threads. Under the hood, it manages a shared thread pool. With GCD, you add blocks of code or work items to dispatch queues and GCD decides which thread to execute them on.
As you structure your code, you’ll find code blocks that can run simultaneously and some that should not. This allows you to use GCD to take advantage of concurrent execution.
Note that GCD decides how much parallelism it requires based on the system and available system resources. It’s important to note that parallelism requires concurrency, but concurrency doesn’t guarantee parallelism.
Basically, concurrency is about structure while parallelism is about execution.
Understanding Queues
As mentioned before, GCD operates on dispatch queues through a class aptly named DispatchQueue
. You submit units of work to this queue, and GCD executes them in a FIFO order (first in, first out), guaranteeing that the first task submitted is the first one started.
Dispatch queues are thread-safe, meaning you can simultaneously access them from multiple threads. GCD’s benefits are apparent when you understand how dispatch queues provide thread safety to parts of your code. The key to this is to choose the right kind of dispatch queue and the right dispatching function to submit your work to the queue.
Queues can be either serial or concurrent. Serial queues guarantee that only one task runs at any given time. GCD controls the execution timing. You won’t know the amount of time between one task ending and the next one beginning:
Concurrent queues allow multiple tasks to run at the same time. The queue guarantees tasks start in the order you add them. Tasks can finish in any order, and you have no knowledge of the time it will take for the next task to start, nor the number of tasks running at any given time.
This is by design: Your code shouldn’t rely on these implementation details.
See the sample task execution below:
Notice how Task 1, Task 2 and Task 3 start quickly, one after the other. On the other hand, Task 1 took a while to start after Task 0. Also notice that while Task 3 started after Task 2, it finished before Task 2.
The decision of when to start a task is entirely up to GCD. If the execution time of one task overlaps with another, it’s up to GCD to determine if it should run on a different core — if one is available — or instead perform a context switch to run a different task.
Understanding Queue Types
GCD provides three main types of queues:
- Main queue: Runs on the main thread and is a serial queue.
- Global queues: Concurrent queues shared by the whole system. Four such queues exist, each with different priorities: high, default, low and background. The background priority queue has the lowest priority and is throttled in any I/O activity to minimize negative system impact.
- Custom queues: Queues you create that can be serial or concurrent. Requests in these queues end up in one of the global queues.
When sending tasks to the global concurrent queues, you don’t specify the priority directly. Instead, you specify a quality of service (QoS) class property. This indicates the task’s importance and guides GCD in determining the priority to give to the task.
The QoS classes are:
- User-interactive: This represents tasks that must complete immediately to provide a nice user experience. Use it for UI updates, event handling and small workloads that require low latency. The total amount of work done in this class during the execution of your app should be small. This should run on the main thread.
- User-initiated: The user initiates these asynchronous tasks from the UI. Use them when the user is waiting for immediate results and for tasks required to continue user interaction. They execute in the high-priority global queue.
- Utility: This represents long-running tasks, typically with a user-visible progress indicator. Use it for computations, I/O, networking, continuous data feeds and similar tasks. This class is designed to be energy efficient. This gets mapped into the low-priority global queue.
- Background: This represents tasks the user isn’t directly aware of. Use it for prefetching, maintenance and other tasks that don’t require user interaction and aren’t time-sensitive. This gets mapped into the background priority global queue.
Scheduling Synchronous vs. Asynchronous Functions
With GCD, you can dispatch a task either synchronously or asynchronously.
A synchronous function returns control to the caller after the task completes. You can schedule a unit of work synchronously by calling DispatchQueue.sync(execute:)
.
An asynchronous function returns immediately, ordering the task to start but not waiting for it to complete. Thus, an asynchronous function doesn’t block the current thread of execution from proceeding to the next function. You can schedule a unit of work asynchronously by calling DispatchQueue.async(execute:)
.
Managing Tasks
You’ve heard about tasks quite a bit by now. For the purposes of this tutorial, you can consider a task to be a closure. Closures are self-contained, callable blocks of code you can store and pass around.
Each task you submit to a DispatchQueue
is a DispatchWorkItem
. You can configure the behavior of a DispatchWorkItem
, such as its QoS class or whether to spawn a new detached thread.
Handling Background Tasks
With all this pent-up GCD knowledge, it’s time for your first app improvement!
Head back to the app and add some photos from your photo library or use the Le Internet option to download a few. Tap a photo. Notice how long it takes for the photo detail view to show up. The lag is more pronounced when viewing large images on slower devices.
Overloading a view controller’s viewDidLoad()
is easy to do, resulting in long waits before the view appears. It’s best to offload work to the background if it’s not absolutely essential at load time.
This sounds like a job for DispatchQueue
‘s async
!
Open PhotoDetailViewController.swift. Modify viewDidLoad()
and replace these two lines:
guard let overlayImage = image.faceOverlayImageFrom() else {
return
}
fadeInNewImage(overlayImage)
With the following code:
// 1
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let overlayImage = self?.image?.faceOverlayImageFrom() else {
return
}
// 2
DispatchQueue.main.async { [weak self] in
// 3
self?.fadeInNewImage(overlayImage)
}
}
Here’s what the code’s doing, step by step:
- You move the work to a background global queue and run the work in the closure asynchronously. This lets
viewDidLoad()
finish earlier on the main thread and makes the loading feel more snappy. Meanwhile, the face detection processing starts and will finish at some later time. - At this point, the face detection processing is complete, and you’ve generated a new image. Since you want to use this new image to update your
UIImageView
, you add a new closure to the main queue. Remember — anything that modifies the UI must run on the main thread! - Finally, you update the UI with
fadeInNewImage(_:)
, which performs a fade-in transition of the new googly eyes image.
In two spots, you add [weak self]
to capture a weak reference to self
in each closure. If you aren’t familiar with capture lists, check out this tutorial on memory management.
Build and run the app. Download photos through the option Le Internet. Select a photo, and you’ll notice that the view controller loads up noticeably faster and adds the googly eyes after a short delay:
This lends a nice before and after effect to the app as the googly eyes appear. Even if you’re trying to load a huge image, your app won’t hang as the view controller loads.
In general, you want to use async
when you need to perform a network-based or CPU-intensive task in the background without blocking the current thread.
Here’s a quick guide on how and when to use the various queues with async
:
-
Main queue: This is a common choice to update the UI after completing work in a task on a concurrent queue. To do this, you code one closure inside another. Targeting the main queue and calling
async
guarantees that this new task will execute sometime after the current method finishes. - Global queue: This is a common choice to perform non-UI work in the background.
-
Custom serial queue: A good choice when you want to perform background work serially and track it. This eliminates resource contention and race conditions since only one task executes at a time. Note that if you need the data from a method, you must declare another closure to retrieve it or consider using
sync
.
Delaying Task Execution
DispatchQueue
allows you to delay task execution. Don’t use this to solve race conditions or other timing bugs through hacks like introducing delays. Instead, use this when you want a task to run at a specific time.
It would be a good idea to display a prompt to the user if there aren’t any photos. You should also consider how users’ eyes will navigate the home screen. If you display a prompt too quickly, they might miss it while their eyes linger on other parts of the view. A two-second delay should be enough to catch users’ attention and guide them.
Open PhotoCollectionViewController.swift and fill in the implementation for showOrHideNavPrompt()
:
// 1
let delayInSeconds = 2.0
// 2
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { [weak self] in
guard let self = self else {
return
}
if !PhotoManager.shared.photos.isEmpty {
self.navigationItem.prompt = nil
} else {
self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
}
// 3
self.navigationController?.viewIfLoaded?.setNeedsLayout()
}
Here’s what’s going on above:
- You specify the amount of time to delay.
- You wait for the specified time, then asynchronously run the block, which updates the photo count and prompt.
- Force the navigation bar layout after setting the prompt to make sure it looks kosher.
showOrHideNavPrompt()
executes in viewDidLoad()
and any time your UICollectionView
reloads.
Build and run the app. There should be a slight delay before you see a prompt displayed:
Why not use Timer
? You could consider using it if you have repeated tasks that are easier to schedule with Timer
. Here are two reasons to stick with dispatch queue’s asyncAfter()
:
- One is readability. To use
Timer
, you have to define a method, then create the timer with a selector or invocation to the defined method. WithDispatchQueue
andasyncAfter()
, you simply add a closure. -
Timer
is scheduled on run loops, so you’d also have to make sure you scheduled it on the correct run loop — and in some cases for the correct run loop modes. In this regard, working with dispatch queues is easier.
Managing Singletons
Singletons are as popular on iOS as cat photos on the web.
One frequent concern with singletons is that often they’re not thread-safe. This concern is justified, given their use: Singletons are often used from multiple controllers accessing the singleton instance simultaneously. Your PhotoManager
class is a singleton, so you’ll need to consider this issue.
Thread-safe code can safely be called from multiple threads or concurrent tasks without causing any problems such as data corruption or app crashes. Code that isn’t thread-safe can only run in one context at a time.
There are two thread-safety cases to consider:
- During initialization of the singleton instance.
- During reads and writes to the instance.
Initialization turns out to be the easy case because of how Swift initializes static properties. It initializes static properties when they’re first accessed, and it guarantees initialization is atomic. That is, Swift treats the code performing initialization as a critical section and guarantees it completes before any other thread gets access to the static property.
A critical section is a piece of code that must not execute concurrently, that is, from two threads at once. This is usually because the code manipulates a shared resource such as a property that can become corrupt if it’s accessed by concurrent processes.
Open PhotoManager.swift to see how you initialize the singleton:
final class PhotoManager {
private init() {}
static let shared = PhotoManager()
}
The private initializer makes sure the only PhotoManager
instance is the one assigned to shared
. This way, you don’t have to worry about syncing changes to your photo store between different managers.
You still have to deal with thread safety when accessing code in the singleton that manipulates shared internal data. Handle this through methods such as synchronizing data access. You’ll see one approach in the next section.
Handling the Readers-Writers Problem
In Swift, any property declared with the let
keyword is a constant and, therefore, read-only and thread-safe. Declare the property with the var
keyword, however, and it becomes mutable and not thread-safe unless the data type is designed to be so. The Swift collection types like Array
and Dictionary
aren’t thread-safe when declared mutable.
Although many threads can read a mutable instance of Array
simultaneously without issue, it’s not safe to let one thread modify the array while another is reading it. Your singleton doesn’t prevent this condition from happening in its current state.
To see the problem, look at addPhoto(_:)
in PhotoManager.swift, which is reproduced below:
func addPhoto(_ photo: Photo) {
unsafePhotos.append(photo)
DispatchQueue.main.async { [weak self] in
self?.postContentAddedNotification()
}
}
This is a write method as it modifies a mutable array object.
Now, look at the photos
property, reproduced below:
private var unsafePhotos: [Photo] = []
var photos: [Photo] {
return unsafePhotos
}
The getter for this property is termed a read method, as it’s reading the mutable array. The caller gets a copy of the array and is protected against inappropriately mutating the original array. However, this doesn’t provide any protection against one thread calling the write method addPhoto(_:)
while another thread simultaneously calls the getter for the photos
property.
That’s why the backing property is named unsafePhotos
— if it’s accessed on the wrong thread, you can get some wacky behavior!
photos
array? In Swift, parameters and return types of functions are either passed by reference or by value.
Passing by value results in a copy of the object, and changes to the copy won’t affect the original. By default in Swift, class
instances are passed by reference and struct
s are passed by value. Swift’s built-in data types like Array
and Dictionary
are implemented as struct
s.
It may look like there’s a lot of copying in your code when passing collections back and forth. Don’t worry about the memory usage implications of this. The Swift collection types are optimized to make copies only when necessary, for instance, when your app modifies an array passed by value for the first time.
Reasoning About Dispatch Barriers
This is the classic software development Readers-Writers Problem. GCD provides an elegant solution of creating a read/write lock using dispatch barriers. Dispatch barriers are a group of functions acting as a serial-style bottleneck when working with concurrent queues.
When you submit a DispatchWorkItem
to a dispatch queue, you can set flags to indicate that it should be the only item executed on the specified queue for that particular time. This means all items submitted to the queue prior to the dispatch barrier must complete before DispatchWorkItem
executes.
When DispatchWorkItem
‘s turn arrives, the barrier executes it and ensures the queue doesn’t execute any other tasks during that time. Once finished, the queue returns to its default implementation.
The diagram below illustrates the effect of a barrier on various asynchronous tasks:
Notice how in normal operation, the queue acts just like a normal concurrent queue. But when the barrier is executing, it essentially acts as a serial queue. That is, the barrier is the only thing executing. After the barrier finishes, the queue goes back to being a normal concurrent queue.
Use caution when using barriers in global background concurrent queues, as these queues are shared resources. Using barriers in a custom serial queue is redundant, as it already executes serially. Using barriers in the custom concurrent queue is a great choice for handling thread safety in atomic or critical areas of code.
You’ll use a custom concurrent queue to handle your barrier function and separate the read and write functions. The concurrent queue will allow multiple read operations simultaneously.
Open PhotoManager.swift and add a private property just above the unsafePhotos
declaration:
private let concurrentPhotoQueue =
DispatchQueue(
label: "com.raywenderlich.GooglyPuff.photoQueue",
attributes: .concurrent)
This initializes concurrentPhotoQueue
as a concurrent queue. You set up label
with a descriptive name that’s helpful during debugging. Typically, you use the reverse DNS style naming convention.
Next, replace addPhoto(_:)
with the following code:
func addPhoto(_ photo: Photo) {
// 1
concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else {
return
}
// 2
self.unsafePhotos.append(photo)
// 3
DispatchQueue.main.async { [weak self] in
self?.postContentAddedNotification()
}
}
}
Here’s how your new write method works:
- You dispatch the write operation asynchronously with a barrier. When it executes, it’s the only item in your queue.
- You add the object to the array.
- Finally, you post a notification that you’ve added the photo. You must post this notification on the main thread because it will do UI work. So you dispatch another task asynchronously to the main queue to trigger the notification.
This takes care of the write, but you also need to implement the photos
read method.
To ensure thread safety with your writes, you need to perform reads on concurrentPhotoQueue
. You need return data from the function call, so an asynchronous dispatch won’t cut it. In this case, sync
would be an excellent candidate.
Use sync
to keep track of your work with dispatch barriers or when you need to wait for the operation to finish before you can use the data processed by the closure.
You need to be careful, though. Imagine if you call sync
and target the current queue you’re already running on. This would result in a deadlock situation.
Two or more items — in most cases, threads — deadlock if they get stuck waiting for each other to complete or perform another action. The first can’t finish because it’s waiting for the second to finish, but the second can’t finish because it’s waiting for the first to finish.
In your case, the sync
call will wait until the closure finishes, but the closure can’t finish — or even start! — until the currently executing closure finishes, which it can’t! This should force you to be conscious of which queue you’re calling from — as well as which queue you’re passing in.
Here’s a quick overview of when and where to use sync
:
- Main queue: Be very careful for the same reasons as above. This situation also has potential for a deadlock condition, which is especially bad on the main queue because the whole app will become unresponsive.
- Global queue: This is a good candidate to sync work through dispatch barriers or when waiting for a task to complete so you can perform further processing.
-
Custom serial queue: Be very careful in this situation. If you’re running in a queue and call
sync
targeting the same queue, you’ll definitely create a deadlock.
Still in PhotoManager.swift, modify the photos
property getter:
var photos: [Photo] {
var photosCopy: [Photo] = []
// 1
concurrentPhotoQueue.sync {
// 2
photosCopy = self.unsafePhotos
}
return photosCopy
}
Here’s what’s going on, step by step:
- Dispatch synchronously onto the
concurrentPhotoQueue
to perform the read. - Store a copy of the photo array in
photosCopy
and return it.
Build and run the app. Download photos through Le Internet. It should behave as before, but underneath the hood, you have some happy threads.
Congratulations — your PhotoManager
singleton is now thread-safe! No matter where or how you read or write photos, you can be confident that it will happen in a safe manner with no surprises.
Where to Go From Here?
You can download the completed version of the project using Download Materials at the top or bottom of this tutorial. In the second part of this tutorial, you’ll continue to improve upon this project.
In this Grand Central Dispatch tutorial, you learned how to make your code thread-safe and maintain the responsiveness of the main thread while performing CPU-intensive tasks.
If you can raise the minimum required version of your app to iOS 15, you should also check how async
and await
works with the WWDC 2021: Intro to async/await video. For a deeper dive, check out our Modern Concurrency in Swift book.
If you plan on optimizing your own apps, you should profile your work with Xcode’s built-in Time Profiler. Using this instrument is outside the scope of this tutorial, so check out How to Use Instruments for an excellent overview.
Our iOS Concurrency with GCD and Operations video course also covers a lot of the same topics covered in this tutorial.
In Part 2 of this tutorial, you’ll dive even deeper into GCD’s API to do even more cool stuff.
If you have any questions or comments, feel free to join the discussion below!