Grand Central Dispatch Tutorial for Swift 5: Part 2/2

Learn all about multithreading, dispatch queues, and concurrency in the second part of this Swift 5 tutorial on Grand Central Dispatch. By David Piper.

Leave a rating/review
Download materials
Save for later
Share
Update note: David Piper updated this tutorial for iOS 15, Swift 5.5 and Xcode 13. Christine Abernathy wrote the original.

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. To do so, you used a combination of dispatch barriers and synchronous dispatch queues. You also enhanced the app’s user experience by using dispatch queues to delay the display of a prompt. This asynchronously offloaded CPU-intensive work when instantiating a view controller.

In this second Grand Central Dispatch tutorial, you’ll work with the same GooglyPuff app you know and love from the first part. You’ll delve into advanced GCD concepts, including:

  • Dispatch groups
  • Canceling dispatch blocks
  • Asynchronous testing techniques
  • Dispatch sources

It’s time to explore some more GCD!

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Pick up where you left off with the sample project from Part One if you followed along.

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:

Although the images didn't finish downloading, the Download Completed alert is already presented.

That’s the first thing you’ll work to fix.

Using Dispatch Groups

Open PhotoManager.swift and check out downloadPhotos(withCompletion:):

func downloadPhotos(
  withCompletion completion: BatchPhotoDownloadingCompletionClosure?
) {
  var storedError: NSError?
  for address in [
    PhotoURLString.overlyAttachedGirlfriend,
    PhotoURLString.successKid,
    PhotoURLString.lotsOfFaces
  ] {
    guard let url = URL(string: address) else { return }
    let photo = DownloadPhoto(url: url) { _, error in
      storedError = error
    }
    PhotoManager.shared.addPhoto(photo)
  }

  completion?(storedError)
}

The completion closure passed into the method fires the alert. You call this after the for loop, which downloads the photos. But, it incorrectly assumes the downloads are complete before you call the closure.

Kick off photo downloads by calling DownloadPhoto(url:). This call returns immediately, but the actual download happens asynchronously. Thus, when completion runs, there’s no guarantee that all the downloads have finished.

What you want is for downloadPhotos(withCompletion:) 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 method, you don’t know when the tasks are complete and they can finish in any order.

Good news! This is exactly why dispatch groups exist. With dispatch groups, you can group together multiple tasks. Then, you can either wait for them to complete or receive a notification once they finish. 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 finish.

In PhotoManager.swift, replace the code in downloadPhotos(withCompletion:) with the following:

// 1
DispatchQueue.global(qos: .userInitiated).async {
  var storedError: NSError?

  // 2
  let downloadGroup = DispatchGroup()
  for address in [
    PhotoURLString.overlyAttachedGirlfriend,
    PhotoURLString.successKid,
    PhotoURLString.lotsOfFaces
  ] {
    guard let url = URL(string: address) else { return }

    // 3
    downloadGroup.enter()
    let photo = DownloadPhoto(url: url) { _, error in
      storedError = error

      // 4
      downloadGroup.leave()
    }   
    PhotoManager.shared.addPhoto(photo)
  }   

  // 5      
  downloadGroup.wait()

  // 6
  DispatchQueue.main.async {
    completion?(storedError)
  }   
}

Here’s what the code is doing step-by-step:

  1. The synchronous wait method blocks the current thread. Thus, you need to use async to place the entire method into a background queue. This ensures you don’t block the main thread.
  2. Create a new dispatch group.
  3. Call enter() to manually notify the group that a task has started. You must balance out the number of enter() calls with the number of leave() calls, or your app will crash.
  4. Notify the group that this work is done.
  5. 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 use wait(timeout:) to specify a timeout and bail out on waiting after a specified time.
  6. At this point, you know 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 the Le Internet option and verify that the alert doesn’t show up until all the images have downloaded.

Two screenshots of the app side by side. The first shows three images, the first two are already downloaded, the last one is still downloading. The second screenshot shows that all three images are downloaded and the Download Completed alert is presented.

Note: The network activities may occur too quickly to discern when the completion closure should be called. If you’re running the app on a device, make sure this really works. You need to toggle some network settings in the Developer section of the iOS Settings. Go to the Network Link Conditioner section, enable it and select a profile. Very Bad Network is a good choice.

If you’re running on iOS Simulator, 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. 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. You don’t want to hold up the main thread, do you? ;] 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.

Using Dispatch Groups, Take 2

Dispatching asynchronously to another queue then blocking work using wait is clumsy. Fortunately, there’s a better way. DispatchGroup can instead notify you when all the group’s tasks are complete.

Still in PhotoManager.swift, replace the code inside downloadPhotos(withCompletion:) with the following:

// 1
var storedError: NSError?
let downloadGroup = DispatchGroup()
for address in [
  PhotoURLString.overlyAttachedGirlfriend,
  PhotoURLString.successKid,
  PhotoURLString.lotsOfFaces
] {
  guard let url = URL(string: address) else { return }
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url) { _, error in
    storedError = error
    downloadGroup.leave()
  }   
  PhotoManager.shared.addPhoto(photo)
}   

// 2    
downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

Here’s what’s going on:

  1. This time, you don’t need to put the method in an async call since you’re not blocking the main thread.
  2. notify(queue:work:) serves as the asynchronous completion closure. It runs when there are no more items left in the group. You also specify that you want to schedule the completion work to 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 after all internet photos have downloaded:

All images are downloaded and the Download Completed alert is presented.

Exploring Concurrency Looping

With all these new tools at your disposal, you should probably thread everything, right?!

Happy bird

Thread ALL THE CODE!

Take a look at downloadPhotos(withCompletion:) 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 like a for loop in that it executes different iterations concurrently. It’s synchronous and returns only when all work is done.

You must take care 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. Striding allows you to do multiple pieces of work for each iteration.

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, though, especially if you need to keep track of progress.

In PhotoManager.swift, replace the code inside downloadPhotos(withCompletion:) with the following:

var storedError: NSError?
let downloadGroup = DispatchGroup()
let addresses = [
  PhotoURLString.overlyAttachedGirlfriend,
  PhotoURLString.successKid,
  PhotoURLString.lotsOfFaces
]
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
  let address = addresses[index]
  guard let url = URL(string: address) else { return }
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url) { _, error in
    storedError = error
    downloadGroup.leave()
  }
  PhotoManager.shared.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

You replaced the former for loop with DispatchQueue.concurrentPerform(iterations:execute:) to handle concurrent looping.

This implementation includes a curious line of code: let _ = DispatchQueue.global(qos: .userInitiated). This causes GCD to use a queue with a .userInitiated quality of service for the concurrent calls.

Build and run the app. Verify that the internet download functionality still behaves properly:

All images are downloaded and the Download Completed alert is presented.

Running this new code on a device will sometimes 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 by running the threads in parallel than you would have by just running the for loop in the first place. You should use DispatchQueue.concurrentPerform(iterations:execute:) for iterating over very large sets, along with the appropriate stride length.
  • You have limited time to create an app — don’t waste time pre-optimizing code that you don’t know is broken. If you’re going to optimize something, do so with 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.

Canceling 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 show this by starting download tasks for several images from Le Internet then canceling some of them.

Still in PhotoManager.swift, replace the code in downloadPhotos(withCompletion:) with the following:

var storedError: NSError?
let downloadGroup = DispatchGroup()
var addresses = [
  PhotoURLString.overlyAttachedGirlfriend,
  PhotoURLString.successKid,
  PhotoURLString.lotsOfFaces
]

// 1
addresses += addresses + addresses

// 2
var blocks: [DispatchWorkItem] = []

for index in 0..<addresses.count {
  downloadGroup.enter()

  // 3
  let block = DispatchWorkItem(flags: .inheritQoS) {
    let address = addresses[index]
    guard let url = URL(string: address) else {
      downloadGroup.leave()
      return
    }
    let photo = DownloadPhoto(url: url) { _, error in
      storedError = error
      downloadGroup.leave()
    }
    PhotoManager.shared.addPhoto(photo)
  }
  blocks.append(block)

  // 4
  DispatchQueue.main.async(execute: block)
}

// 5
for block in blocks[3..<blocks.count] {

  // 6
  let cancel = Bool.random()
  if cancel {

    // 7
    block.cancel()

    // 8
    downloadGroup.leave()
  }
}

downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

Here’s a step-by-step walk-through of the code above:

  1. You expand the addresses array to hold three copies of each image.
  2. You initialize a blocks array to hold dispatch block objects for later use.
  3. You create a new DispatchWorkItem. You pass in a flags parameter to specify that the block should inherit its Quality of Service class from the queue you dispatch it to. Then, you define the work to do in a closure.
  4. 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. Thus, you know that the download blocks will execute at some later time.
  5. You skip the first three download blocks by slicing the blocks array.
  6. Here, you use Bool.random() to randomly pick between true and false. It's like a coin toss.
  7. If the random value is true, you cancel the block. This can only cancel blocks that are still in a queue and haven't began executing. You can't cancel a block in the middle of execution.
  8. 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. You cancel some of the additional image downloads in the queue before they start.

All images are downloaded and the Download Completed alert is presented. This time there are 5 instead of 3 images downloaded. Two images were downloaded multiple times, one was only downloaded once.

This is a pretty contrived example, but it's a nice illustration of how to use — and cancel — dispatch blocks.

Dispatch blocks 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 that often, they can be helpful in the right situations.

Testing Asynchronous Code

This might sound like a crazy idea, but did you know that Xcode has testing functionality? :] Writing and running tests is important when building complex relationships in code.

Xcode tests are all contained in subclasses of XCTestCase. They're any method whose signature begins with test. Tests run on the main thread, so you can assume that every test happens in a serial manner.

As soon as a given test method completes, Xcode considers the test to have finished and moves on to the next test. This 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.

Take a brief look at how you can use semaphores to test asynchronous code.

Using 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 also 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 downloadImageURL(withString:) with the following:

let url = try XCTUnwrap(URL(string: urlString))

// 1
let semaphore = DispatchSemaphore(value: 0)
_ = DownloadPhoto(url: url) { _, error in
  if let error = error {
    XCTFail("\(urlString) failed. \(error.localizedDescription)")
  }

  // 2
  semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)

// 3
if semaphore.wait(timeout: timeout) == .timedOut {
  XCTFail("\(urlString) timed out")
} 

Here's how the semaphore works in the code above:

  1. You create a semaphore and set its start value. This represents the number of things that can access the semaphore without needing to increment it. Another name for incrementing a semaphore is signaling it.
  2. You signal the semaphore in the completion closure. This increments its count and signals that the semaphore is available to other resources.
  3. You wait on the semaphore with a given timeout. This call blocks the current thread until you signal the semaphore. A non-zero return code from this function means that the timeout period expired. In this case, the test fails because 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 Command-U, if you have the default key bindings. They should all succeed:

Screenshot of Xcode showing GooglyPuffTests. All tests succeeded.

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, turn off your connection. The tests complete with a fail result after 10 seconds. It worked!

Screenshot of Xcode showing GooglyPuffTests. All tests failed. An error alert shows that the internet connection appears to be offline.

These are rather trivial tests. But if you're working with a server team, they can prevent finger-pointing of who is to blame for the latest network issue.

Note: When you implement asynchronous tests in your code, look at XCTWaiter first before going down to these low-level APIs. XCTWaiter's APIs are much nicer and provide a lot of powerful technology for asynchronous testing.

Using Dispatch Sources

Dispatch sources are a particularly interesting feature of GCD. You can use a dispatch source 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 check. You also need to define the dispatch queue on which its event handler block should execute. You then assign an event handler to the dispatch source.

Upon creation, dispatch sources start off in a suspended state. This allows you to perform any extra configuration required — such as setting up the event handler. After configuring your dispatch source, you must 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 goes into debug mode.

Open PhotoCollectionViewController.swift and add the following just below the backgroundImageOpacity global property declaration:

// 1
#if DEBUG

  // 2
  var signal: DispatchSourceSignal?

  // 3
  private let setupSignalHandlerFor = { (_ object: AnyObject) in
    let queue = DispatchQueue.main

    // 4
    signal =
      DispatchSource.makeSignalSource(signal: SIGSTOP, queue: queue)
        
    // 5
    signal?.setEventHandler {
      print("Hi, I am: \(object.description ?? "")")
    }

    // 6
    signal?.resume()
  }
#endif

The code is a little involved, so walk through it step-by-step:

  1. You compile this code only in debug mode to prevent "interested parties" from gaining a lot of insight into your app. :] Add -D DEBUG under Project Settings ▸ Build Settings ▸ Swift Compiler - Custom Flags ▸ Other Swift Flags ▸ Debug. It should be set already in the starter project.
  2. Declare signal variable of type DispatchSourceSignal for use in monitoring Unix signals.
  3. Create a block assigned to the setupSignalHandlerFor global property. You use it for one-time setup of your dispatch source.
  4. Here, you set up signal. You're interested in monitoring the SIGSTOP Unix signal. The main queue hands received events — you'll discover why shortly.
  5. Next, 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.
  6. 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() 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 pressing the Pause then Play buttons in Xcode's debugger:

Screenshot of Xcode. The pause execution button is highlighted.

Check out the console. Here's what you'll see:

Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7fbf0af08a10>

Your 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 app.

An interesting idea is to use this approach as a stack trace tool to find the object you want to manipulate in the debugger.

Cool bird with sunglasses

Time to flex some GCD muscles.

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 any time 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!

Note: If you haven't already noticed which threads are which in the debugger, take a look at them now. The main thread will always be the first thread. It's followed by libdispatch, the coordinator for GCD, as the second thread. After that, the thread count and remaining threads depend on what the hardware was doing when the app hit the breakpoint.

In the debugger console, type the following:

expr object.navigationItem.prompt = "WOOT!"

The Xcode debugger can sometimes be uncooperative. You might get this message:

error: use of unresolved identifier 'self'

If you do, 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:

po object

Then, manually cast the value to the type you want by running the following commands in the debugger, replacing 0xHEXADDRESS with the outputted address:

expr let $vc = unsafeBitCast(0xHEXADDRESS, to: GooglyPuff.PhotoCollectionViewController.self)
expr $vc.navigationItem.prompt = "WOOT!"

If this doesn't work, lucky you – you encountered another bug in LLDB! In that case, you may have to try building and running the app again.

Once you've run this command successfully, resume execution of the app. You'll see the following:

The initial view controller, but this time the navigation bar shows the text WOOT!.

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 version of the project using the Download Materials button at the top or bottom of this tutorial.

Beyond GCD, you should check out Operation and OperationQueue 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. Operation offers better control, an implementation for handling maximum concurrent operations and a more object-oriented paradigm at the cost of speed.

Also, take a look at the iOS Concurrency with GCD and Operations video course, which covers a lot of the same topics covered in this tutorial.

If you'd like to explore how to use async and await in your code, please check out our book, Modern Concurrency in Swift.

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." :]

If you have any questions or comments, please join the forum discussion below!