Update note: This tutorial has been updated for Swift 3 by Christine Abernathy. The previous tutorial was written by Bjørn Ruud.
Welcome to the second and final part of this Grand Central Dispatch tutorial series!
In the first part of this series, you learned about concurrency, threading, and how GCD works. You made a singleton thread safe for reading and writing using a combination of dispatch barriers and synchronous dispatch queues. You also enhanced the app’s UX by using dispatch queues to delay the display of a prompt and to asynchronously offload CPU intensive work when instantiating a view controller.
In this second Grand Central Dispatch tutorial, you’ll be working with the same GooglyPuff application you know and love from the first part. You’ll delve into advanced GCD concepts including dispatch groups, cancelling dispatch blocks, asynchronous testing techniques, and dispatch sources.
It’s time to explore some more GCD!
Getting Started
You can pick up where you left off with the sample project from part one if you followed along. Alternatively, you can download the finished project from the first part of this tutorial here.
Run the app, tap +, and select Le Internet to add internet photos. You may notice that a download completion alert message pops up well before the images have finished downloading:
That’s the first thing you’ll work on fixing.
Dispatch Groups
Open PhotoManager.swift and check out downloadPhotosWithCompletion(_:)
:
func downloadPhotosWithCompletion( _ completion: BatchPhotoDownloadingCompletionClosure?) { var storedError: NSError? for address in [overlyAttachedGirlfriendURLString, successKidURLString, lotsOfFacesURLString] { let url = URL(string: address) let photo = DownloadPhoto(url: url!) { _, error in if error != nil { storedError = error } } PhotoManager.sharedManager.addPhoto(photo) } completion?(storedError) } |
The alert is fired by the completion
closure passed into the method. This is called after the for
loop where the photos are downloaded. You’ve incorrectly assumed that the downloads are complete before the closure is called.
Photo downloads are kicked off by calling DownloadPhoto(url:)
. This call returns immediately but the actual download happens asynchronously. Therefore when completion
is called, there’s no guarantee that all the downloads are done.
What you want is for downloadPhotosWithCompletion(_:)
to call its completion closure after all the photo download tasks are complete. How can you monitor these concurrent asynchronous events to achieve this? With the current methodology, you don’t know when the tasks are complete and they can finish in any order.
Good news! This is exactly what dispatch groups are designed to handle. With dispatch groups you can group together multiple tasks and either wait for them to be completed or be notified once they are complete. Tasks can be asynchronous or synchronous and can even run on different queues.
DispatchGroup
manages dispatch groups. You’ll first look at its wait
method. This blocks your current thread until all the group’s enqueued tasks have been completed.
In PhotoManager.swift and replace the code in downloadPhotosWithCompletion(_:)
with the following:
DispatchQueue.global(qos: .userInitiated).async { // 1 var storedError: NSError? let downloadGroup = DispatchGroup() // 2 for address in [overlyAttachedGirlfriendURLString, successKidURLString, lotsOfFacesURLString] { let url = URL(string: address) downloadGroup.enter() // 3 let photo = DownloadPhoto(url: url!) { _, error in if error != nil { storedError = error } downloadGroup.leave() // 4 } PhotoManager.sharedManager.addPhoto(photo) } downloadGroup.wait() // 5 DispatchQueue.main.async { // 6 completion?(storedError) } } |
Here’s what the code is doing step-by-step:
- Since you’re using the synchronous
wait
method which blocks the current thread, you useasync
to place the entire method into a background queue to ensure you don’t block the main thread. - This creates a new dispatch group.
- You call
enter()
to manually notify the group that a task has started. You must balance out the number ofenter()
calls with the number ofleave()
calls or your app will crash. - Here you notify the group that this work is done.
- You call
wait()
to block the current thread while waiting for tasks’ completion. This waits forever which is fine because the photos creation task always completes. You can usewait(timeout:)
to specify a timeout and bail out on waiting after a specified time. - At this point, you are guaranteed that all image tasks have either completed or timed out. You then make a call back to the main queue to run your completion closure.
Build and run the app. Download photos through Le Internet option and verify that the alert doesn’t show up until all the images are downloaded.
If you are running on the Simulator, you can use the Network Link Conditioner included in the Advanced Tools for Xcode to change your network speed. This is a good tool to have in your arsenal because it forces you to be conscious of what happens to your apps when connection speeds are less than optimal.
Dispatch groups are a good candidate for all types of queues. You should be wary of using dispatch groups on the main queue if you’re waiting synchronously for the completion of all work since you don’t want to hold up the main thread. However, the asynchronous model is an attractive way to update the UI once several long-running tasks finish, such as network calls.
Your current solution is good, but in general it’s best to avoid blocking threads if at all possible. Your next task is to rewrite the same method to notify you asynchronously when all the downloads have completed.
Dispatch Groups, Take 2
Dispatching asynchronously to another queue then blocking work using wait
is clumsy. Fortunately, there is a better way. DispatchGroup
can instead notify you when all the group’s tasks are complete.
Still in PhotoManager.swift, replace the code inside downloadPhotosWithCompletion(_:)
with the following:
// 1 var storedError: NSError? let downloadGroup = DispatchGroup() for address in [overlyAttachedGirlfriendURLString, successKidURLString, lotsOfFacesURLString] { let url = URL(string: address) downloadGroup.enter() let photo = DownloadPhoto(url: url!) { _, error in if error != nil { storedError = error } downloadGroup.leave() } PhotoManager.sharedManager.addPhoto(photo) } downloadGroup.notify(queue: DispatchQueue.main) { // 2 completion?(storedError) } |
Here’s what’s going on:
- In this new implementation you don’t need to surround the method in an
async
call since you’re not blocking the main thread. notify(queue:work:)
serves as the asynchronous completion closure. It is called when there are no more items left in the group. You also specify that you want to schedule the completion work to be run on the main queue.
This is a much cleaner way to handle this particular job as it doesn’t block any threads.
Build and run the app. Verify that the download complete alert is still displayed afer all internet photos are downloaded:
Concurrency Looping
With all of these new tools at your disposal, you should probably thread everything, right!?
Take a look at downloadPhotosWithCompletion(_:)
in PhotoManager
. You might notice that there’s a for
loop in there that cycles through three iterations and downloads three separate images. Your job is to see if you can run this for
loop concurrently to try and speed things up.
This is a job for DispatchQueue.concurrentPerform(iterations:execute:)
. It works similarly to a for
loop in that it executes different iterations concurrently. It is sychronous and returns only when all of the work is done.
Care must be taken when figuring out the optimal number of iterations for a given amount of work. Many iterations and a small amount of work per iteration can create so much overhead that it negates any gains from making the calls concurrent. The technique known as striding helps you out here. This is where for each iteration you do multiple pieces of work.
When is it appropriate to use DispatchQueue.concurrentPerform(iterations:execute:)
? You can rule out serial queues because there’s no benefit there – you may as well use a normal for
loop. It’s a good choice for concurrent queues that contain looping, especially if you need to keep track of progress.
In PhotoManager.swift replace the code inside downloadPhotosWithCompletion(_:)
with the following:
var storedError: NSError? let downloadGroup = DispatchGroup() let addresses = [overlyAttachedGirlfriendURLString, successKidURLString, lotsOfFacesURLString] let _ = DispatchQueue.global(qos: .userInitiated) DispatchQueue.concurrentPerform(iterations: addresses.count) { i in let index = Int(i) let address = addresses[index] let url = URL(string: address) downloadGroup.enter() let photo = DownloadPhoto(url: url!) { _, error in if error != nil { storedError = error } downloadGroup.leave() } PhotoManager.sharedManager.addPhoto(photo) } downloadGroup.notify(queue: DispatchQueue.main) { completion?(storedError) } |
The former for
loop has been replaced with DispatchQueue.concurrentPerform(iterations:execute:)
to handle concurrent looping.
Build and run the app. Verify that the internet download functionality still behaves properly:
Running this new code on the device will occasionally produce marginally faster results. But was all this work worth it?
Actually, it’s not worth it in this case. Here’s why:
- You’ve probably created more overhead running the threads in parallel than just running the
for
loop in the first place. You should useDispatchQueue.concurrentPerform(iterations:execute:)
for iterating over very large sets along with the appropriate stride length. - Your time to create an app is limited — don’t waste time pre-optimizing code that you don’t know is broken. If you’re going to optimize something, optimize something that is noticeable and worth your time. Find the methods with the longest execution times by profiling your app in Instruments. Check out How to Use Instruments in Xcode to learn more.
- Typically, optimizing code makes your code more complicated for yourself and for other developers coming after you. Make sure the added complication is worth the benefit.
Remember, don’t go crazy with optimizations. You’ll only make it harder on yourself and others who have to wade through your code.
Cancelling Dispatch Blocks
Thus far, you haven’t seen code that allows you to cancel enqueued tasks. This is where dispatch block objects represented by DispatchWorkItem
comes into focus. Be aware that you can only cancel a DispatchWorkItem
before it reaches the head of a queue and starts executing.
Let’s demonstrate this by starting download tasks for several images from Le Internet then cancelling some of them.
Still in PhotoManager.swift, replace the code in downloadPhotosWithCompletion(_:)
with the following:
var storedError: NSError? let downloadGroup = DispatchGroup() var addresses = [overlyAttachedGirlfriendURLString, successKidURLString, lotsOfFacesURLString] addresses += addresses + addresses // 1 var blocks: [DispatchWorkItem] = [] // 2 for i in 0 ..< addresses.count { downloadGroup.enter() let block = DispatchWorkItem(flags: .inheritQoS) { // 3 let index = Int(i) let address = addresses[index] let url = URL(string: address) let photo = DownloadPhoto(url: url!) { _, error in if error != nil { storedError = error } downloadGroup.leave() } PhotoManager.sharedManager.addPhoto(photo) } blocks.append(block) DispatchQueue.main.async(execute: block) // 4 } for block in blocks[3 ..< blocks.count] { // 5 let cancel = arc4random_uniform(2) // 6 if cancel == 1 { block.cancel() // 7 downloadGroup.leave() // 8 } } downloadGroup.notify(queue: DispatchQueue.main) { completion?(storedError) } |
Here’s a step-by-step walk through the code above:
- You expand the
addresses
array to hold three copies of each image. - You initialize a
blocks
array to hold dispatch block objects for later use. - You create a new
DispatchWorkItem
. You pass in aflags
parameter to specify that the block should inherit its Quality of Service class from the queue it is dispatched to. You then define the work to be done in a closure. - You dispatch the block asynchronously to the main queue. For this example, using the main queue makes it easier to cancel select blocks since it’s a serial queue. The code that sets up the dispatch blocks is already executing on the main queue so you are guaranteed that the download blocks will execute at some later time.
- You skip the first three download blocks by slicing the
blocks
array. - Here you use
arc4random_uniform()
to randomly pick a number between 0 and 1. It’s like a coin toss. - If the random number is 1 you cancel the block. This can only cancel blocks that are still in a queue and haven’t began executing. Blocks can’t be canceled in the middle of execution.
- Here you remember to remove the canceled block from the dispatch group.
Build and run the app, then add images from Le Internet. You’ll see that the app now downloads more than three images. The number of extra images changes each time you re-run your app. Some of the additional image downloads in the queue are canceled before they start.
This is a pretty contrived example but it a nice illustration of how dispatch block objects are used and cancelled.
Dispatch block objects can do a lot more, so be sure to check out Apple’s documentation.
Miscellaneous GCD Fun
But wait! There’s more! Here are some extra functions that are a little farther off the beaten path. Although you won’t use these tools nearly as frequently, they can be tremendously helpful in the right situations.
Testing Asynchronous Code
This might sound like a crazy idea, but did you know that Xcode has testing functionality? :] I know, sometimes I like to pretend it’s not there, but writing and running tests is important when building complex relationships in code.
Testing in Xcode is performed on subclasses of XCTestCase
and runs any method in its method signature that begins with test
. Testing is measured on the main thread, so you can assume that every test happens in a serial manner.
As soon as a given test method completes, XCTest methods will consider a test to be finished and move onto the next test. That means that any asynchronous code from the previous test will continue to run while the next test is running.
Networking code is usually asynchronous, since you don’t want to block the main thread while performing a network fetch. That, coupled with the fact that tests finish when the test method finishes, can make it hard to test networking code.
Let’s take a brief look at two common techniques for testing asynchronous code: one using semaphores and one using expectations.
Semaphores
Semaphores are an old-school threading concept introduced to the world by the ever-so-humble Edsger W. Dijkstra. Semaphores are a complex topic because they build upon the intricacies of operating system functions.
If you want to learn more about semaphores, check out this detailed discussion on semaphore theory. If you’re the academic type, you may want to check out Dining Philosophers Problem which is a classic software development problem that uses semaphores.
Open GooglyPuffTests.swift and replace the code inside downloadImageURLWithString(_:)
with the following:
let url = URL(string: urlString) let semaphore = DispatchSemaphore(value: 0) // 1 let _ = DownloadPhoto(url: url!) { _, error in if let error = error { XCTFail("\(urlString) failed. \(error.localizedDescription)") } semaphore.signal() // 2 } let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds) if semaphore.wait(timeout: timeout) == .timedOut { // 3 XCTFail("\(urlString) timed out") } |
Here’s how the semaphore works in the code above:
- You create a semaphore and set its start value. This represents the number of things that can access the semaphore without needing the semaphore to be incremented (note that incrementing a semaphore is known as signaling it).
- You signal the semaphore in the completion closure. This increments the semaphore count and signals that the semaphore is available to other resources that want it.
- You wait on the semaphore, with a given timeout. This call blocks the current thread until the semaphore has been signaled. A non-zero return code from this function means that the timeout was reached. In this case, the test is failed because it is deemed that the network should not take more than 10 seconds to return — a fair point!
Run your tests by selecting Product\Test from the menu or using ⌘+U if you have the default key bindings. They should all succeed in a timely manner:
Disable your connection and run the tests again. If you’re running on a device, put it in airplane mode. If you’re running on the simulator then simply turn off your connection. The tests complete with a fail result after 10 seconds. Great, it worked!
These are rather trivial tests, but if you’re working with a server team then these basic tests can prevent a wholesome round of finger-pointing of who is to blame for the latest network issue.
Expectations
The XCTest
framework provides another solution to the asynchronous code testing problem in the form of expectations. This feature lets you set up an expectation – something you expect will happen – and then start an asynchronous task. Then you can have the test runner wait until the asynchronous task marks the expectation as fulfilled.
Still working in GooglyPuffTests.swift, replace the code in downloadImageURLWithString(_:)
with the following:
let url = URL(string: urlString) let downloadExpectation = expectation(description: "Image downloaded from \(urlString)") // 1 let _ = DownloadPhoto(url: url!) { _, error in if let error = error { XCTFail("\(urlString) failed. \(error.localizedDescription)") } downloadExpectation.fulfill() // 2 } waitForExpectations(timeout: 10) { // 3 error in if let error = error { XCTFail(error.localizedDescription) } } |
Here’s how it works:
- You create the expectation with
expectation(description:)
. The test runner will display the string parameter in the test log upon failure, so describe what you expect to happen. - You call
fulfill()
in the closure that executes asynchronously to mark the expectation as fulfilled. - You wait for expectations to be fulfilled by calling
waitForExpectations(timeout:handler:)
. If the wait times out, it’s treated as an error.
Run the tests with a good network connection. You should see a summary logged to Xcode’s console like this:
Test Suite 'All tests' passed at 2016-12-01 02:32:57.179. Executed 3 tests, with 0 failures (0 unexpected) in 10.666 (10.672) seconds |
Now disable your network connection and re-run the tests. You should see an error message and summary results logged that looks like this:
[GooglyPuffTests.GooglyPuffTests testLotsOfFacesImageURL] : failed - http://i.imgur.com/tPzTg7A.jpg failed. The Internet connection appears to be offline. ... Test Suite 'All tests' failed at 2016-12-01 02:35:10.055. Executed 3 tests, with 3 failures (0 unexpected) in 0.061 (0.082) seconds |
The end result is not very different from using a semaphore, but leveraging the XCTest
framework is a cleaner and more readable solution.
Dispatch Sources
Dispatch sources are a particularly interesting feature of GCD. A dispatch source can basically be used to monitor for some type of event. Events can include Unix signals, file descriptors, Mach ports, VFS Nodes, and other obscure stuff.
When setting up a dispatch source, you tell it what type of events you want to monitor and the dispatch queue on which its event handler block should be executed. You then assign an event handler to the dispatch source.
Upon creation, dispatch sources start off in a suspended state. This allows for additional configuration steps to take place, for example setting up the event handler. Once you’ve configured your dispatch source, you should resume it to start processing events.
In this tutorial, you’ll get a small taste of working with dispatch sources by using it in a rather peculiar way: to monitor when your app is put into debug mode.
Open PhotoCollectionViewController.swift and add the following just below the backgroundImageOpacity
global property declaration:
#if DEBUG // 1 var signal: DispatchSourceSignal? // 2 private let setupSignalHandlerFor = { (_ object: AnyObject) -> Void in // 3 let queue = DispatchQueue.main signal = DispatchSource.makeSignalSource(signal: Int32(SIGSTOP), queue: queue) // 4 signal?.setEventHandler { // 5 print("Hi, I am: \(object.description!)") } signal?.resume() // 6 } #endif |
The code is a little involved, so walk through it step-by-step:
- You compile this code only in DEBUG mode to prevent “interested parties” from gaining a lot of insight into your app. :] DEBUG is defined by adding -D DEBUG under Project Settings -> Build Settings -> Swift Compiler – Custom Flags -> Other Swift Flags -> Debug. It should be set already in the starter project.
- You declare a
signal
variable of typeDispatchSourceSignal
for use in monitoring Unix signals. - You create a block assigned to the
setupSignalHandlerFor
global variable that you’ll use for one-time setup of your dispatch source. - Here you set up
signal
. You indicate that you’re interested in monitoring theSIGSTOP
Unix signal and handling received events on the main queue — you’ll discover why shortly. - If the dispatch source is successfully created, you register an event handler closure that’s invoked whenever you receive the
SIGSTOP
signal. Your handler prints a message that includes the class description. - All sources start off in the suspended state by default. Here you tell the dispatch source to resume so it can start monitoring events.
Add the following code to viewDidLoad()
just below the call to super.viewDidLoad()
:
#if DEBUG _ = setupSignalHandlerFor(self) #endif |
This code invokes the dispatch source’s initialization code.
Build and run the app. Pause the program execution and resume the app immediately by tapping the pause then play buttons in Xcode’s debugger:
Check out the console. You should see something like this:
Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7fbf0af08a10> |
You app is now debugging-aware! That’s pretty awesome, but how would you use this in real life?
You could use this to debug an object and display data whenever you resume the app. You could also give your app custom security logic to protect itself (or the user’s data) when malicious attackers attach a debugger to your application.
An interesting idea is to use this approach as a stack trace tool to find the object you want to manipulate in the debugger.
Think about that situation for a second. When you stop the debugger out of the blue, you’re almost never on the desired stack frame. Now you can stop the debugger at anytime and have code execute at your desired location. This is very useful if you want to execute code at a point in your app that’s tedious to access from the debugger. Try it out!
Put a breakpoint on the print()
statement inside the setupSignalHandlerFor
block that you just added.
Pause in the debugger, then start again. The app will hit the breakpoint you added. You’re now deep in the depths of your PhotoCollectionViewController
method. Now you can access the instance of PhotoCollectionViewController
to your heart’s content. Pretty handy!
In the debugger console, type the following:
(lldb) expr object.navigationItem.prompt = "WOOT!" |
The Xcode debugger can sometimes be uncooperative. If you get the message:
error: use of unresolved identifier 'self' |
Then you have to do it the hard way to work around a bug in LLDB. First take note of the address of object
in the debug area:
(lldb) po object |
Then manually cast the value to the type you want:
(lldb) expr let $vc = unsafeBitCast(0x7fbf0af08a10, to: GooglyPuff.PhotoCollectionViewController.self) (lldb) expr $vc.navigationItem.prompt = "WOOT!" |
Now resume execution of the app. You’ll see the following:
With this method, you can make updates to the UI, inquire about the properties of a class, and even execute methods — all while not having to restart the app to get into that special workflow state. Pretty neat.
Where To Go From Here?
You can download the completed project for this Grand Central Dispatch tutorial here.
I hate to bang on this subject again, but you really should check out the How to Use Instruments tutorial. You’ll definitely need this if you plan on doing any optimization of your apps. Be aware that Instruments is good for profiling relative execution: comparing which areas of code takes longer in relation to other areas. If you’re trying to figure out the actual execution time of a method, you might need to come up with a more home-brewed solution.
Also check out How to Use NSOperations and NSOperationQueue Tutorial in Swift, a concurrency technology that is built on top of GCD. In general, it’s best practice to use GCD if you are using simple fire-and-forget tasks. NSOperations offers better control, an implementation for handling maximum concurrent operations, and a more object-oriented paradigm at the cost of speed.
You should also take a look at our iOS Concurrency with GCD and Operations video tutorial series, which covers a lot of the same topics that we’ve covered in this tutorial.
Remember, unless you have a specific reason to go lower, always try and stick with a higher level API. Only venture into the dark arts of Apple if you want to learn more or to do something really, really “interesting”. :]
Good luck and have fun! Post any questions or feedback in the discussion below!
The post Grand Central Dispatch Tutorial for Swift 3: Part 2/2 appeared first on Ray Wenderlich.