Chapters

Hide chapters

Concurrency by Tutorials

Third Edition · iOS 16 · Swift 5.7 · Xcode 14

3. Queues & Threads
Written by Scott Grosch

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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

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.

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

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.

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

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

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)

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 {
  guard let self else { return }

  // Perform your work here
  // ...

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

Image Loading Example

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

Using a Global Queue

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

private func downloadWithGlobalQueue() {
  DispatchQueue.global(qos: .utility).async {
  }
}
DispatchQueue.main.async {
  image = Image(uiImage: uiImage)
}

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:

private func downloadWithUrlSession() {
  URLSession.shared.dataTask(with: url) { data, _, _ in
    guard let data, let uiImage = UIImage(data: data) else {
      return
    }

    DispatchQueue.main.async {
      image = Image(uiImage: uiImage)
    }
  }
  .resume()
}

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

let queue = DispatchQueue(label: "xyz")
queue.async {
  print("The block of code ran!")
}
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:

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)

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.

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.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now