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

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

Version

  • Swift 4.2, iOS 12, Xcode 10
Update note: Evan Dekhayser updated this tutorial to Swift 4.2. 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 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, canceling 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 use the Download Materials button at the top or bottom of this tutorial to download the starter project.

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 downloadPhotos(withCompletion:):

func downloadPhotos(
    withCompletion completion: BatchPhotoDownloadingCompletionClosure?) {
  var storedError: NSError?
  for address in [PhotoURLString.overlyAttachedGirlfriend,
                  PhotoURLString.successKid,
                  PhotoURLString.lotsOfFaces] {
                    let url = URL(string: address)
                    let photo = DownloadPhoto(url: url!) { _, error in
                      if error != nil {
                        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 where the photos are downloaded. You’ve incorrectly assumed that the downloads are complete before you call the closure.

You kick off photo downloads by calling DownloadPhoto(url:). This call returns immediately but the actual download happens asynchronously. Therefore 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 methodology, 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 and either wait for them to complete, or receive a notification once they 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 finish.

In PhotoManager.swift and 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] {
    let url = URL(string: address)

    // 3
    downloadGroup.enter()
    let photo = DownloadPhoto(url: url!) { _, error in
      if error != nil {
        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. Since you’re using the synchronous wait method which blocks the current thread, you use async to place the entire method into a background queue to ensure 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. Here you notify the group that this work is done.
  5. 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 use wait(timeout:) to specify a timeout and bail out on waiting after a specified time.
  6. 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 have downloaded.

grand central dispatch tutorial

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

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 downloadPhotos(withCompletion:) with the following:

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

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

Here’s what’s going on:

  1. In this new implementation, you don’t need to surround 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:

Concurrency Looping

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

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 similarly to a for loop in that it executes different iterations concurrently. It is synchronous and returns only when all of the 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, 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]
  let url = URL(string: address)
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url!) { _, error in
    if error != nil {
      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). Calling this tells 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:

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 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, 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.

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 demonstrate 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]
    let url = URL(string: address)
    let photo = DownloadPhoto(url: url!) { _, error in
      if error != nil {
        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 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 so you are guaranteed 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.

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

Xcode tests are all contained in subclasses of XCTestCase and are 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.

Let's take a brief look at how you can use semaphores to test asynchronous code.

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

let url = URL(string: urlString)

// 1
let semaphore = DispatchSemaphore(value: 0)
let _ = 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 the semaphore to be incremented (note that incrementing a semaphore is known as signaling it).
  2. 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.
  3. You wait on the semaphore, with a given timeout. This call blocks the current thread until the semaphore is signaled. 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 in a timely manner:

grand central dispatch tutorial

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!

grand central dispatch tutorial

These are rather trivial tests, but if you're working with a server team, these basic tests can prevent a wholesome round of 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.

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 monitor and 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 additional configuration required such as setting up the event handler. Once you've configured 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. :] 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.
  2. You declare a signal variable of type DispatchSourceSignal for use in monitoring Unix signals.
  3. You create a block assigned to the setupSignalHandlerFor global variable that you'll use for one-time setup of your dispatch source.
  4. Here you set up signal. You indicate that you're interested in monitoring the SIGSTOP Unix signal and handling received events on the main queue — you'll discover why shortly.
  5. 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.
  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() 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:

grand central dispatch tutorial

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.

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

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, 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. 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:

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:

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, I recommend you 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.

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!

Contributors

Comments