Update note: This tutorial has been updated for Swift 3 by Christine Abernathy. The previous tutorial was written by Bjørn Ruud.
Grand Central Dispatch (GCD) is a low-level API for managing concurrent operations. GCD can help you improve your app’s responsiveness by defering computationally expensive tasks to the background. It’s an easier concurrency model to work with than locks and threads.
In Swift 3, GCD got a major revamp, moving from a C-based API to a “Swiftier” API that included new classes and new data structures.
In this two-part Grand Central Dispatch tutorial, you’ll learn the ins and outs of GCD. This first part will explain what GCD does and showcase 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 application 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 to apply this effect on from your Photo Library or select images downloaded from the internet.
Your mission in this tutorial, if you choose to accept it, is to use GCD to optimize the app and ensure you can safely call code from different threads.
Getting Started
Download the starter project for this tutorial and unzip it. Run the project in Xcode to see what you have to work with.
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.
There are four classes that you’ll primarily be working with in this tutorial:
- PhotoCollectionViewController: The initial view controller. It displays the selected photos as thumbnails.
- PhotoDetailViewController: Displays a selected photo from
PhotoCollectionViewController
and adds googly eyes to the image. - Photo: This is a protocol describing the properties of a photo. It provides an image, thumbnail and their corresponding statuses. Two classes are provided which implement the protocol:
DownloadPhoto
which instantiate a photo from an instance ofURL
, andAssetPhoto
which instantiates a photo from an instance ofPHAsset
. - PhotoManager: This manages all the
Photo
objects.
There are a few problems with the app. One that you may have noticed when running the app 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.
GCD Concepts
To understand GCD, you need to be comfortable with several concepts related to concurrency and threading.
Concurrency
In iOS a process or application is made up of one or more threads. The threads are managed independently by the operating system scheduler. Each thread can execute concurrently but it’s up to the system to decide if this happens and how it happens.
Single-core devices can achieve concurrency through time-slicing. They would 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 then allows you to use GCD to take advantage of concurrent execution.
Note that GCD decides how much parallelism is required based on the system and available system resources. It’s important to note that parallelism requires concurrency, but concurrency does not guarantee parallelism.
Basically, concurrency is about structure while parallelism is about execution.
Queues
GCD provides dispatch queues represented by DispatchQueue
to manage tasks you submit and execute them in a FIFO order guaranteeing that the first task submitted is the first one started.
Dispatch queues are thread-safe which means that you can access them from multiple threads simultaneously. The benefits of GCD are apparent when you understand how dispatch queues provide thread safety to parts of your own 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. Tasks are guaranteed to start in the order they were added. 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 that are running at any given time.
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 first.
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 to perform a context switch to run a different task.
GCD provides three main types of queues:
- Main queue: runs on the main thread and is a serial queue.
- Global queues: concurrent queues that are shared by the whole system. There are four such queues with different priorities : high, default, low, and background. The background priority queue is I/O throttled.
- Custom queues: queues that you create which can be serial or concurrent. These actually trickle down into being handled by one of the global queues.
When setting up the global concurrent queues, you don’t specify the priority directly. Instead you specify a Quality of Service (QoS) class property. This will indicate the task’s importance and guide GCD into determining the priority to give to the task.
The QoS classes are:
- User-interactive: This represents tasks that need to be done immediately in order 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 represents tasks that are initiated from the UI and can be performed asynchronously. It should be used when the user is waiting for immediate results, and for tasks required to continue user interaction. This will get mapped into 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, continous data feeds and similar tasks. This class is designed to be energy efficient. This will get mapped into the low priority global queue.
- Background: This represents tasks that the user is not directly aware of. Use it for prefetching, maintenance, and other tasks that don’t require user interaction and aren’t time-sensitive. This will get mapped into the background priority global queue.
Synchronous vs. Asynchronous
With GCD, you can dispatch a task either synchronously or asynchronously.
A synchronous function returns control to the caller after the task is completed.
An asynchronous function returns immediately, ordering the task to be done but not waiting for it. Thus, an asynchronous function does not block the current thread of execution from proceeding on to the next function.
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 that can be stored and passed around.
Tasks that you submit to a DispatchQueue
are encapsulated by 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 on 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 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:
let overlayImage = faceOverlayImageFromImage(image) fadeInNewImage(overlayImage) |
With the following code:
DispatchQueue.global(qos: .userInitiated).async { // 1 let overlayImage = self.faceOverlayImageFromImage(self.image) DispatchQueue.main.async { // 2 self.fadeInNewImage(overlayImage) // 3 } } |
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 is started 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 – you must always accessUIKit
classes on the main thread! - Finally, you update the UI with
fadeInNewImage(_:)
which performs a fade-in transition of the new googly eyes image.
Build and run the app. Download photos through Le Internet option. 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 are added. Even if you were trying to load an insanely huge image, your app wouldn’t hang as the view controller is loaded.
In general, you’ll want to use async
when you need to perform a network-based or CPU intensive task in the background and not block the current thread.
Here’s a quick guide of 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’ll 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 since you know only one task at a time is executing. Note that if you need the data from a method, you must inline another closure to retrieve it or consider using
sync
.
Delaying Task Execution
DispatchQueue
allows you to delay task execution. Care should be taken not to use this to solve race conditions or other timing bugs through hacks like introducing delays. Use this when you want a task to run at a specific time.
Consider the user experience of your app for a moment. It’s possible that users might be confused about what to do when they open the app for the first time — were you? :]
It would be a good idea to display a prompt to the user if there aren’t any photos. You should also consider how the user’s eyes will navigate the home screen. If you display a prompt too quickly, they might miss it as their eyes linger on other parts of the view. A one-second delay before displaying the prompt should be enough to catch the user’s attention and guide them.
Open PhotoCollectionViewController.swift and fill in the implementation for showOrHideNavPrompt()
:
let delayInSeconds = 1.0 // 1 DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { // 2 let count = PhotoManager.sharedManager.photos.count if count > 0 { self.navigationItem.prompt = nil } else { self.navigationItem.prompt = "Add photos with faces to Googlyify them!" } } |
Here’s what’s going on above:
- You specify a variable for the amount of time to delay.
- You then wait for the specified time then asynchronously run the block which updates the photos count and updates the prompt.
showOrHideNavPrompt()
executes in viewDidLoad()
and anytime your UICollectionView
is reloaded.
Build and run the app. There should be a slight delay before a prompt is displayed:
Wondering when it’s appropriate to use asyncAfter
? Generally it’s a good choice to use it in the main queue. You’ll want to use caution when using asyncAfter
on other queues such as the global background queues or a custom serial queue. You’re better off sticking to the main queue.
Why not use Timer? You could consider using it if you have repeated tasks which 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. With DispatchQueue
and asyncAfter
you simply add a closure.
Timer
is scheduled on run loops so you would also have to make sure it was scheduled on the run loop you want it to fire (and in some cases for the correct run loop modes). In this regard, working with dispatch queues is easier.
Managing Singletons
Singletons. Love them or hate them, they’re as popular in 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 at the same time. Your PhotoManager
class is a singleton, so you’ll need to consider this issue.
Thread safe code can be safely called from multiple threads or concurrent tasks without causing any problems such as data corruption, or app crashes. Code that is not thread safe must only be run in one context at a time.
There are two thread safety cases to consider, during initialization of the singleton instance and during reads and writes to the instance.
Initialization turns out to be the easy case because of how Swift initializes global variables. Global variables are initialized when they are first accessed, and they are guaranteed to be initialized in an atomic fashion. That is, the code performing initialization is treated as a critical section and is guaranteed to complete before any other thread gets access to the global variable.
A critical section is a piece of code that must not be executed concurrently, that is, from two threads at once. This is usually because the code manipulates a shared resource such as a variable that can become corrupt if it’s accessed by concurrent processes.
Open up PhotoManager.swift to see how the singleton is initialized:
private let _sharedManager = PhotoManager() |
The private global _sharedManager
variable is used to initialize PhotoManager
lazily. This happens only on the first access which you can see here:
class var sharedManager: PhotoManager { return _sharedManager } |
The public sharedManager
variable returns the private _sharedManager
variable. Swift ensures that this operation is thread safe.
You still have to deal with thread safety when accessing code in the singleton that manipulates shared internal data. You can 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 variable declared with the let
keyword is considered a constant and is read-only and thread-safe. Declare the variable 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
are not 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, take a look at addPhoto(_:)
in PhotoManager.swift, which has been reproduced below:
func addPhoto(_ photo: Photo) { _photos.append(photo) DispatchQueue.main.async { self.postContentAddedNotification() } } |
This is a write method as it modifies a mutable array object.
Now take a look at the photos
property, reproduced below:
fileprivate var _photos: [Photo] = [] var photos: [Photo] { return _photos } |
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 mutating the original array inappropriately. This does not provide any protection against one thread calling the write method addPhoto(_:)
while simultaneously another thread calls the getter for the photos
property.
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 will not affect the original. By default in Swift class instances are passed by reference and structs passed by value. Swift’s built-in data types like Array
and Dictionary
, are implemented as structs.
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 only make copies when necessary, for instance when an array passed by value is modified for the first time after being passed.
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 that all items submitted to the queue prior to the dispatch barrier must complete before the DispatchWorkItem
will execute.
When the DispatchWorkItem
‘s turn arrives, the barrier executes it and ensures that the queue does not 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 like 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 custom concurrent queue is a great choice for handling thread safety in atomic of 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 _photos
declaration:
fileprivate let concurrentPhotoQueue = DispatchQueue( label: "com.raywenderlich.GooglyPuff.photoQueue", // 1 attributes: .concurrent) // 2 |
This initializes concurrentPhotoQueue
as a concurrent queue.
- You set up
label
with a descriptive name that is helpful during debugging. Typically you’ll use the reversed DNS style naming convention. - You specify a concurrent queue.
Next, replace addPhoto(_:)
with the following code:
func addPhoto(_ photo: Photo) { concurrentPhotoQueue.async(flags: .barrier) { // 1 self._photos.append(photo) // 2 DispatchQueue.main.async { // 3 self.postContentAddedNotification() } } } |
Here’s how your new write function works:
- You dispatch the write operation asynchronously with a barrier. When it executes, it will be the only item in your queue.
- You add the object to the array.
- Finally you post a notification that you’ve added the photo. This notification should be posted 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 the concurrentPhotoQueue
queue. 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 will result in a deadlock situation.
Two (or sometimes more) items — in most cases, threads — are said to be deadlocked if they all 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 (it can’t even start!) until the currently executing closure is finished, which 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.
- 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]! concurrentPhotoQueue.sync { // 1 photosCopy = self._photos // 2 } 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 option. It should behave as before but underneath the hood, you have some very 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 be done in a safe manner with no surprises.
Where To Go From Here?
In this Grand Central Dispatch tutorial, you learned how to make your code thread safe and how to maintain the responsiveness of the main thread while performing CPU intensive tasks.
You can download the completed project which contains all the improvements made in this tutorial so far. In the second part of this tutorial you’ll continue to improve upon this project.
If you plan on optimizing your own apps, you really should be profiling your work with the Time Profile template in Instruments. Using this utility is outside the scope of this tutorial, so check out How to Use Instruments for a excellent overview.
Also make sure that you profile with an actual device, since testing on the Simulator can give very different results that are different from what your users will experience.
You may also want to check out this excellent talk by Rob Pike on Concurrency vs Parallelism.
Our iOS Concurrency with GCD and Operations video tutorial series also covers a lot of the same topics that we’ve covered in this tutorial.
In the next part 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!
The post Grand Central Dispatch Tutorial for Swift 3: Part 1/2 appeared first on Ray Wenderlich.