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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Exploring Concurrency Looping

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

Thread ALL THE CODE!

Happy bird

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.