Chapters

Hide chapters

Concurrency by Tutorials

Second Edition · iOS 13 · Swift 5.1 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

3. Queues & Threads
Written by Scott Grosch

Dispatch queues and threads have been mentioned a couple of times now, and you’re probably wondering what they are at this point. In this chapter, you’ll get a much deeper understanding of what Dispatch queue and Threads are, and how to best incorporate them in your development workflow.

Threads

You’ve probably heard the term multithreading at some point, yes? A thread is really short for thread of execution, and it’s how a running process splits tasks across resources on the system. Your iOS app is a process that runs multiple tasks by utilizing multiple threads. You can have as many threads executing at once as you have cores in your device’s CPU.

There are many advantages to splitting your app’s work into multiple threads:

  • Faster execution: By running tasks on threads, it’s possible for work to be done concurrently, which will allow it to finish faster than running everything serially.
  • Responsiveness: If you only perform user-visible work on the main UI thread, then users won’t notice that the app slows down or freezes up periodically due to work that could be performed on another thread.
  • Optimized resource consumption: Threads are highly optimized by the OS.

Sounds great, right? More cores, more threads, faster app. I bet you’re ready to learn how to create one, right? Too bad! In reality, you should never find yourself needing to create a thread explicitly. The OS will handle all thread creation for you using higher abstractions.

Apple provides the APIs necessary for thread management, but if you try to directly manage them yourself, you could in fact degrade, rather than improve, performance. The OS keeps track of many statistics to know when it should and should not allocate or destroy threads. Don’t fool yourself into thinking it’s as simple as spinning up a thread when you want one. For those reasons, this book will not cover direct thread management.

Dispatch queues

The way you work with threads is by creating a DispatchQueue. When you create a queue, the OS will potentially create and assign one or more threads to the queue. If existing threads are available, they can be reused; if not, then the OS will create them as necessary.

Creating a dispatch queue is pretty simple on your part, as you can see in the example below:

let label = "com.raywenderlich.mycoolapp.networking"
let queue = DispatchQueue(label: label)

Phew, fairly easy, eh? Normally, you’d put the text of the label directly inside the initializer, but it’s broken into separate statements for the sake of brevity.

The label argument simply needs to be any unique value for identification purposes. While you could simply use a UUID to guarantee uniqueness, it’s best to use a reverse-DNS style name, as shown above (e.g. com.company.app), since the label is what you’ll see when debugging and it’s helpful to assign it meaningful text.

The main queue

When your app starts, a main dispatch queue is automatically created for you. It’s a serial queue that’s responsible for your UI. Because it’s used so often, Apple has made it available as a class variable, which you access via DispatchQueue.main. You never want to execute something synchronously against the main queue, unless it’s related to actual UI work. Otherwise, you’ll lock up your UI which could potentially degrade your app performance.

If you recall from the previous chapter, there are two types of dispatch queues: serial or concurrent. The default initializer, as shown in the code above, will create a serial queue wherein each task must complete before the next task is able to start.

In order to create a concurrent queue, simply pass in the .concurrent attribute, like so:

let label = "com.raywenderlich.mycoolapp.networking"
let queue = DispatchQueue(label: label, attributes: .concurrent)

Concurrent queues are so common that Apple has provided six different global concurrent queues, depending on the Quality of service (QoS) the queue should have.

Quality of service

When using a concurrent dispatch queue, you’ll need to tell iOS how important the tasks are that get sent to the queue so that it can properly prioritize the work that needs to be done against all the other tasks that are clamoring for resources. Remember that higher-priority work has to be performed faster, likely taking more system resources to complete and requiring more energy than lower-priority work.

If you just need a concurrent queue but don’t want to manage your own, you can use the global class method on DispatchQueue to get one of the pre-defined global queues:

let queue = DispatchQueue.global(qos: .userInteractive)

As mentioned above, Apple offers six quality of service classes:

.userInteractive

The .userInteractive QoS is recommended for tasks that the user directly interacts with. UI-updating calculations, animations or anything needed to keep the UI responsive and fast. If the work doesn’t happen quickly, things may appear to freeze. Tasks submitted to this queue should complete virtually instantaneously.

.userInitiated

The .userInitiated queue should be used when the user kicks off a task from the UI that needs to happen immediately, but can be done asynchronously. For example, you may need to open a document or read from a local database. If the user clicked a button, this is probably the queue you want. Tasks performed in this queue should take a few seconds or less to complete.

.utility

You’ll want to use the .utility dispatch queue for tasks that would typically include a progress indicator such as long-running computations, I/O, networking or continuous data feeds. The system tries to balance responsiveness and performance with energy efficiency. Tasks can take a few seconds to a few minutes in this queue.

.background

For tasks that the user is not directly aware of you should use the .background queue. They don’t require user interaction and aren’t time sensitive. Prefetching, database maintenance, synchronizing remote servers and performing backups are all great examples. The OS will focus on energy efficiency instead of speed. You’ll want to use this queue for work that will take significant time, on the order of minutes or more.

.default and .unspecified

There are two other possible choices that exist, but you should not use explicitly. There’s a .default option, which falls between .userInitiated and .utility and is the default value of the qos argument. It’s not intended for you to directly use. The second option is .unspecified, and exists to support legacy APIs that may opt the thread out of a quality of service. It’s good to know they exist, but if you’re using them, you’re almost certainly doing something wrong.

Note: Global queues are always concurrent and first-in, first-out.

Inferring QoS

If you create your own concurrent dispatch queue, you can tell the system what the QoS is via its initializer:

let queue = DispatchQueue(label: label, 
                          qos: .userInitiated,
                          attributes: .concurrent)

However, this is like arguing with your spouse/kids/dogs/pet rock: Just because you say it doesn’t make it so! The OS will pay attention to what type of tasks are being submitted to the queue and make changes as necessary.

If you submit a task with a higher quality of service than the queue has, the queue’s level will increase. Not only that, but all the operations enqueued will also have their priority raised as well.

If the current context is the main thread, the inferred QoS is .userInitiated. You can specify a QoS yourself, but as soon as you’ll add a task with a higher QoS, your queue’s QoS service will be increased to match it.

Adding task to queues

Dispatch queues provide both sync and async methods to add a task to a queue. Remember that, by task, I simply mean, “Whatever block of code you need to run.” When your app starts, for example, you may need to contact your server to update the app’s state. That’s not user initiated, doesn’t need to happen immediately and depends on networking I/O, so you should send it to the global utility queue:

DispatchQueue.global(qos: .utility).async { [weak self] in
  guard let self = self else { return }

  // Perform your work here
  // ...

  // Switch back to the main queue to
  // update your UI
  DispatchQueue.main.async {
    self.textLabel.text = "New articles available!"
  }
}

There are two key points you should take away from the above code sample. First, there’s nothing special about a DispatchQueue that nullifies the closure rules. You still need to make sure that you’re properly handling the closure’s captured variables, such as self, if you plan to utilize them.

Strongly capturing self in a GCD async closure will not cause a reference cycle (e.g. a retain cycle) since the whole closure will be deallocated once it’s completed, but it will extend the lifetime of self. For instance, if you make a network request from a view controller that has been dismissed in the meantime, the closure will still get called. If you capture the view controller weakly, it will be nil. However, if you capture it strongly, the view controller will remain alive until the closure finishes its work. Keep that in mind and capture weakly or strongly based on your needs.

Second, notice how updates to the UI are dispatched to the main queue inside the dispatch to the background queue. It’s not only OK, but very common, to nest async type calls inside others.

Note: You should never perform UI updates on any queue other than the main queue. If it’s not documented what queue an API callback uses, dispatch it to the main queue!

Use extreme caution when submitting a task to a dispatch queue synchronously. If you find yourself calling the sync method, instead of the async method, think once or twice whether that’s really what you should be doing. If you submit a task synchronously to the current queue, which blocks the current queue, and your task tries to access a resource in the current queue, then your app will deadlock, which is explained more in Chapter 5, “Concurrency Problems.” Similarly, if you call sync from the main queue, you’ll block the thread that updates the UI and your app will appear to freeze up.

Note: Never call sync from the main thread, since it would block your main thread and could even potentially cause a deadlock.

Image loading example

You’ve been inundated with quite a bit of theoretical concepts at this point. Time to see an actual example!

In the downloadable materials for this book, you’ll find a starter project for this chapter. Open up the Concurrency.xcodeproj project. Build and run the app. You’ll see some images slowly load from the network into a UICollectionView. If you try to scroll the screen while the images are loading, either nothing will happen or the scrolling will be very slow and choppy, depending on the speed of the device you are using.

Open up CollectionViewController.swift and take a look at what’s going on. When the view loads, it just grabs a static list of image URLs to be displayed. In a production app, of course, you’d likely be making a network call at this point to generate a list of items to display, but for this example it’s easier to hardcode a list of images.

The collectionView(_:cellForItemAt:) method is where the trouble happens. You can see that when a cell is ready to be displayed a call is made via one of Data’s constructors to download the image and then it’s assigned to the cell. The code looks simple enough, and it is what most starting iOS developers would do to download an image, but you saw the results: a choppy, underperforming UI experience!

Unless you slept through the previous pages of explanation, you know by now that the work to download the image, which is a network call, needs to be done on a separate thread from the UI.

Mini-challenge: Which queue do you think should handle the image download? Take a look back a few pages and make your decision.

Did you pick either .userInteractive or .userInitiated? It’s tempting to do because the end result is directly visible to the user but the reality is if you used that logic then you’d never use any other queue. The proper choice here is to use the .utility queue. You’ve got no control over how long a network call will take to complete and you want the OS to properly balance the speed vs. battery life of the device.

Using a global queue

Create a new method in CollectionViewController that starts off like so:

private func downloadWithGlobalQueue(at indexPath: IndexPath) {
  DispatchQueue.global(qos: .utility).async { [weak self] in
  }
}

You’ll eventually call this from collectionView(_:cellForItemAt:) to perform the actual image processing. Begin by determining which URL should be loaded. Since the list of URLs are part of self, you’ll need to handle normal closure capture semantics. Add the following code inside the async closure:

guard let self = self else {
  return
}

let url = self.urls[indexPath.item]

Once you know the URL to load, you can use the same Data initializer you previously used. Even though it’s an synchronous operation that’s being performed, it is running on a separate thread and thus the UI isn’t impacted. Add the following to the end of the closure:

guard let data = try? Data(contentsOf: url),
      let image = UIImage(data: data) else {
  return
}

Now that you’ve successfully downloaded the contents of the URL and turned it into a UIImage, it’s time to apply it to the collection view’s cell. Remember that updates to the UI can only happen on the main thread! Add this async call to the end of the closure:

DispatchQueue.main.async {
  if let cell = self.collectionView.cellForItem(at: indexPath) as? PhotoCell {
    cell.display(image: image)
  }
}

Notice that the bare minimum of code is being sent back to the main thread. Do every bit of work that you can before dispatching to the main queue so that your UI remains as responsive as possible. Is the cell assignment confusing you? Why not just pass the actual PhotoCell to this method instead of an IndexPath?

Consider the nature of what you’re doing here. You’ve offloaded the configuration of the cell to an asynchronous process. While the network download is occurring, the user is very likely doing something with your app. In the case of a UITableView or UICollectionView, that probably means that they’re doing some scrolling. By the time the network call finishes, the cell might have been reused for another image, or it might have been disposed of completely. By calling cellForItem(at:), you’re grabbing the cell at the time you’re ready to update it. If it still exists and if it’s still on the screen, then you’ll update the display. If it’s not, then nil will be returned.

Had you instead simply passed in a PhotoCell and directly interacted with that object, you’d have discovered that random images are placed in random cells, and you’ll see the same image repeated multiple times as you scroll around.

Now that you’ve got a proper image download and cell configuration method, update collectionView(_:cellForItemAt:) to call it. Replace everything in-between creating and returning the cell with these two lines of code:

cell.display(image: nil)
downloadWithGlobalQueue(at: indexPath)

Build and run your app again. Once your app starts loading images, scroll the table view. Notice how silky smooth that scrolling is! Of course, you might notice some issues: the images pop in and out of existence, load pretty slowly and keep being reloaded when you scroll away. You’ll need a way to start and cancel these requests and cache their results to make the experience perfect. These are all things which are much easier using operations instead of Grand Central Dispatch, which you’ll cover in later chapters. So keep reading! :]

Using built-in methods

You can see how simple the above changes were to vastly improve the performance of your app. However, it’s not always necessary to grab a dispatch queue yourself. Many of the standard iOS libraries handle that for you. Add the following method to CollectionViewController:

private func downloadWithUrlSession(at indexPath: IndexPath) {
  URLSession.shared.dataTask(with: urls[indexPath.item]) {
    [weak self] data, response, error in

    guard let self = self,
          let data = data,
          let image = UIImage(data: data) else {
      return
    }

    DispatchQueue.main.async {
      if let cell = self.collectionView
        .cellForItem(at: indexPath) as? PhotoCell {
        cell.display(image: image)
      }
    }
  }.resume()
}

Notice how, this time, instead of getting a dispatch queue, you directly used the dataTask method on URLSession. The code is almost the same, but it handles the download of the data for you so that you don’t have to do it yourself, nor do you need to grab a dispatch queue. Always prefer to use the system provided methods when they are available as it will make your code not only more future-proof but easier to read for other developers. A junior programmer might not understand what the dispatch queues are, but they understand making a network call.

If you call downloadWithUrlSession(at:) instead of downloadWithGlobalQueue(at:) in collectionView(_:cellForItemAt:) you should see the exact same result after building and running your app again.

DispatchWorkItem

There’s another way to submit work to a DispatchQueue besides passing an anonymous closure. DispatchWorkItem is a class that provides an actual object to hold the code you wish to submit to a queue.

For example, the following code:

let queue = DispatchQueue(label: "xyz")
queue.async {
  print("The block of code ran!")
}

Would work exactly like this piece of code:

let queue = DispatchQueue(label: "xyz")
let workItem = DispatchWorkItem {
  print("The block of code ran!")
}
queue.async(execute: workItem)

Canceling a work item

One reason you might wish to use an explicit DispatchWorkItem is if you have a need to cancel the task before, or during, execution. If you call cancel() on the work item one of two actions will be performed:

  1. If the task has not yet started on the queue, it will be removed.
  2. If the task is currently executing, the isCancelled property will be set to true.

You’ll need to check the isCancelled property periodically in your code and take appropriate action to cancel the task, if possible.

Poor man’s dependencies

The DispatchWorkItem class also provides a notify(queue:execute:) method which can be used to identify another DispatchWorkItem that should be executed after the current work item completes.

let queue = DispatchQueue(label: "xyz")
let backgroundWorkItem = DispatchWorkItem { }
let updateUIWorkItem = DispatchWorkItem { }

backgroundWorkItem.notify(queue: DispatchQueue.main,
                          execute: updateUIWorkItem)
queue.async(execute: backgroundWorkItem)

Notice that when specifying a follow-on work item to be executed, you must explicitly specify which queue the work item should execute against.

If you find yourself needing the ability to cancel a task or specify dependencies, I strongly suggest you instead refer to Chapter 9, “Operation Dependencies” and Chapter 10, “Canceling Operations” in Section III, “Operations.”

Where to go from here?

At this point, you should have a good grasp of what dispatch queues are, what they’re used for and how to use them. Play around with the code samples from above to ensure you understand how they work.

Consider passing the PhotoCell into the download methods instead of just passing in the IndexPath to see a common type of bug in practice.

The sample app is of course somewhat contrived so as to easily showcase how a DispatchQueue works. There are many other performance improvements that could be made to the sample app but those will have to wait for Chapter 7, “Operation Queues.”

Now that you’ve seen the benefits, the next chapter will introduce you to the dangers of implementing concurrency in your app.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.