Mastering Grand Central Dispatch

Main and Global Dispatch Queues

The application we worked with in the previous episodes creates and manages a serial dispatch queue and a concurrent dispatch queue. Creating a dispatch queue manually is fine, but it isn't always necessary.

Grand Central Dispatch manages a handful of dispatch queues your application can use. The main dispatch queue is one of these dispatch queues. In this episode, we find out which dispatch queues Grand Central Dispatch provides and when you should consider using them.

Main Dispatch Queue

The main dispatch queue is one of the dispatch queues that is managed by Grand Central Dispatch. Your application can access the main dispatch queue through the main class computed property of the DispatchQueue class. We briefly discussed the main dispatch queue earlier in this series. Let's take a closer look. What is special about the main dispatch queue? Why is it important?

Remember that Grand Central Dispatch doesn't make a guarantee as to which thread is used to execute a block of work. That's fine, but it also introduces a problem. Let's revisit the application from the previous episode. Remember that the application fetches the data for each image in the background to ensure that the main thread isn't blocked. That's a common and effective strategy to build applications that are both performant and responsive.

Earlier in this series, we ran into an issue, though. We tried to update the image property of an image view from a background thread. That's a red flag because it can result in a undefined or unexpected behavior. The user interface should always be updated on the main thread. That brings us to the question "How do you ensure the update takes place on the main thread?" That's where the main dispatch queue comes into play.

When the user launches your application, the system creates a main thread for your application. To make it easy for your application to dispatch work to the main thread, Grand Central Dispatch provides the main dispatch queue. As we discussed in the previous episodes, work submitted to the main dispatch queue is guaranteed to be executed on the main thread.

Threads and Dispatch Queues

It is very important to make a clear distinction between a thread and a dispatch queue. The main thread and the main dispatch queue are often used interchangeably, but they are not the same.

Remember that a dispatch queue is not tied to a particular thread. Grand Central Dispatch decides which thread services a block of work that is submitted to a dispatch queue. There is one exception, that is, the main dispatch queue. Because work submitted to the main dispatch queue is guaranteed to be executed on the main thread, we can say that the main dispatch queue is bound to the main thread.

But that doesn't mean that the main thread is also bound to the main dispatch queue. When the main thread is idle, Grand Central Dispatch can decide to use the main thread to execute work scheduled onto a concurrent dispatch queue. That is fine and it shows that the main thread isn't bound to the main dispatch queue.

Global Dispatch Queues

Grand Central Dispatch also provides a collection of concurrent dispatch queues. These dispatch queues are often referred to as global dispatch queues because they are global to the application. Accessing a global dispatch queue is just as easy as accessing the main dispatch queue. We invoke the global() class method on the DispatchQueue class to ask Grand Central Dispatch for a reference to a global dispatch queue.

Open ViewController.swift and navigate to the viewDidLoad() method. Let's execute the blocks of work in the for loop on a global dispatch queue instead of the concurrent dispatch queue of the ViewController instance.

override func viewDidLoad() {
    super.viewDidLoad()

    print("Start \(Date())")

    for (index, imageView) in imageViews.enumerated() {
        // Fetch URL
        let url = images[index]

        DispatchQueue.global().async { [weak self] in
            // Populate Image View
            self?.loadImage(with: url, for: imageView)
        }
    }

    print("Finish \(Date())")
}

Build and run the application to see the result. The behavior of the application hasn't changed because the global dispatch queue is also a concurrent dispatch queue. The images are still being fetched in the background concurrently.

The global() class method defines an optional parameter of type DispatchQoS.QoSClass. Qos stands for quality of service and the DispatchQoS struct encapsulates quality of service classes. The QoSClass enum defines a number of quality of service classes.

When the application asks Grand Central Dispatch for a dispatch queue, it can optionally specify the quality of service class of the dispatch queue. That brings us to the question "What is a quality of service class?" We discuss quality of service classes later in this series, but I want to make sure you understand why quality of service classes are important.

Quality of service classes are an integral aspect of Grand Central Dispatch. Unfortunately, many developers are not familiar with quality of service classes or don't use them. A quality of service class defines the importance of a block of work. Grand Central Dispatch defines several quality of service classes to help developers communicate to Grand Central Dispatch how important a block of work is. It takes the quality of service class into account when it schedules a block of work for execution.

We can update the implementation of the viewDidLoad() method by specifying the quality of service class of the global dispatch queue. We ask Grand Central Dispatch for the dispatch queue with a quality of service class of utility.

override func viewDidLoad() {
    super.viewDidLoad()

    print("Start \(Date())")

    for (index, imageView) in imageViews.enumerated() {
        // Fetch URL
        let url = images[index]

        DispatchQueue.global(qos: .utility).async { [weak self] in
            // Populate Image View
            self?.loadImage(with: url, for: imageView)
        }
    }

    print("Finish \(Date())")
}

Enable the first breakpoint of the loadImage(with:for:) method and run the application. Open the Debug Navigator and wait for the debugger to hit the breakpoint.

Global Dispatch Queue

The Debug Navigator shows that the data for the image is being fetched on a background thread. While this isn't new, notice that the dispatch queue that dispatched the block of work to the background thread has a label that reads com.apple.root.utility-qos. The label confirms that the block of work was submitted to a global dispatch queue with a quality of service class of utility. The Debug Navigator also shows that it's a concurrent dispatch queue.

The default quality of service class is default. This means that Grand Central Dispatch should infer the quality of service class of the block of work that is submitted to the dispatch queue. That will make more sense when we discuss quality of service classes in detail.

Which Dispatch Queue Should You Use?

As the name implies, global dispatch queue are global to the application. This means that they can be accessed from anywhere in your application. That may seem convenient, but it isn't always what you want.

It can be useful to create a dedicated dispatch queue for a particular task or a group of tasks, such as data synchronization. By using a dedicated dispatch queue, the owner of the dispatch queue can carefully control which objects have access to the dispatch queue.

The ViewController class defines two private dispatch queues. The ViewController instance is the only object that has access to these dispatch queues, which means that the work submitted to these dispatch queues is carefully controlled.

Earlier in this series, I emphasized that a concurrent dispatch queue can execute blocks of work concurrently. When a block of work scheduled onto a concurrent dispatch queue is executed, depends on the resources of the system and many other factors. If your application submits dozens or hundreds of blocks of work to a dispatch queue, then don't expect those blocks of work to be executed concurrently. Remember that a processor core executes one command at a time, which means that the number of active threads is limited by the number of cores. And your application isn't the only process that's running on the system.

Carefully scheduling the work that needs to be carried out is important. Don't rely on the system to optimize everything for you.

What's Next?

The main dispatch queue is very convenient if you need to execute a task on the main thread. The global dispatch queues that Grand Central Dispatch provides are useful if you need to execute a task in the background, but that doesn't mean you can't or shouldn't create a dispatch queue manually if you need one. The needs of your application define when to use which type of dispatch queue.

Next Episode "Synchronous and Asynchronous Execution"