Mastering Grand Central Dispatch

Serial and Concurrent Dispatch Queues

You should now have a fundamental understanding of dispatch queues. A dispatch queue is responsible for managing the execution of blocks of work. Grand Central Dispatch determines which thread is used for the execution of a block of work. Developers unfamiliar with Grand Central Dispatch often wrongly assume that a dispatch queue is tied to a particular thread. Remember that Grand Central Dispatch doesn't make a guarantee as to which thread is used for the execution of a block of work submitted to a dispatch queue.

Earlier in this series, I explained that Grand Central Dispatch manages a pool of threads. It uses the pool of threads for the execution of the blocks of work that are submitted to dispatch queues. It's Grand Central Dispatch that decides which thread is used for the execution of a block of work. It takes a multitude of factors into account when it makes that decision. We revisit this aspect of Grand Central Dispatch several more times in this series.

Serial Dispatch Queues

Download the starter project of this episode. Build and run the application in the simulator or on a physical device. The application loads the images one by one. Do you notice anything interesting? Run the application several times. There's one detail that should stand out. The application loads the images from top to bottom. This happens every single time. There's an explanation for this behavior.

The dispatch queue of the view controller is a serial dispatch queue. A serial dispatch queue executes one block of work at a time and it does this in FIFO order, first in, first out.

Let's take a look at the implementation of the viewDidLoad() method of the ViewController class. The application iterates through the collection of image views from top to bottom. For each image view, it loads an image and updates the image view on the main thread.

Let me illustrate this with a diagram. The blocks of work are submitted to the serial dispatch queue of the view controller. Because a dispatch queue is a FIFO queue, block A is dequeued first. Grand Central Dispatch brings up a worker thread, a background thread, to service block A. Block B is executed when block A has completed executing. In other words, the serial dispatch queue executes one block at a time. Block C is executed when block B has completed executing and block D is executed when block C has completed executing.

How does a serial dispatch queue work?

The diagram shows that each block of work is executed on a different thread. While that is a possibility, it's also possible that the same background thread is used for the execution of the other blocks of work that were submitted to the serial dispatch queue. Remember that Grand Central Dispatch doesn't make a guarantee as to which thread is used to service a block of work.

A serial dispatch queue has a few interesting features. The order in which the blocks of work are executed is predictable. That isn't surprising since every dispatch queue is a FIFO queue. More interesting is that the order in which the blocks of work finish executing is also predictable.

Remember from the previous episode that the main dispatch queue is a serial dispatch queue, which means that the main dispatch queue shares these features.

Concurrent Dispatch Queues

Serial dispatch queues are interesting, but Grand Central Dispatch would be limited in functionality if it only managed serial dispatch queues. Grand Central Dispatch also manages another type of dispatch queues, concurrent dispatch queues. As the name implies, a concurrent dispatch queue executes blocks of work concurrently or at the same time. Let me illustrate this with an example.

Open ViewController.swift and rename the dispatchQueue property to serialDispatchQueue. Update the label of the dispatch queue to Serial Dispatch Queue. The current implementation of the application teaches us that a dispatch queue is by default a serial dispatch queue. That's important to know.

private let serialDispatchQueue = DispatchQueue(label: "Serial Dispatch Queue")

We also need to update the implementation of the viewDidLoad() method.

override func viewDidLoad() {
    super.viewDidLoad()

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

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

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

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

Define another property and name it concurrentDispatchQueue. We initialize a DispatchQueue instance with label Concurrent Dispatch Queue. The main difference with the serial dispatch queue is that we pass a value for the attributes parameter. We specify that the dispatch queue should be a concurrent dispatch queue.

private let concurrentDispatchQueue = DispatchQueue(label: "Concurrent Dispatch Queue", attributes: .concurrent)

Let's find out how a concurrent dispatch queue differs from a serial dispatch queue. Revisit the viewDidLoad() method and submit the blocks of work in the for loop to the concurrent dispatch queue instead of the serial dispatch queue.

override func viewDidLoad() {
    super.viewDidLoad()

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

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

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

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

Disable the breakpoints we set in the loadImage(with:for:) method and run the application. What do you notice? It's immediately obvious that the order in which the images are displayed isn't from top to bottom. In fact, the order in which the images are displayed is unpredictable. Every time you run the application, the order can be different. The order in which the images are displayed no longer depends exclusively on the type of dispatch queue. It also depends on other factors, such as the speed of the network and the available resources of the system.

The images that are included in the application's bundle are displayed almost immediately after the application has finished launching. Fetching the images that are located on the remote server takes longer and it takes a few moments for the application to display them to the user.

Exploring Concurrent Dispatch Queues

As I mentioned earlier, a concurrent dispatch queue can execute the blocks of work that are submitted concurrently or at the same time. It's important to emphasize that it can execute blocks of work concurrently. Whether it executes blocks of work concurrently depends on the available resources of the system. I talk more about that later in this series.

A concurrent dispatch queue is also a FIFO queue, which means that, like a serial dispatch queue, the order of execution is predictable. The order in which blocks of work are completed is unpredictable.

Take a look at this diagram for a better understanding of this concept. The application submits four blocks of work to the concurrent dispatch queue in the viewDidLoad() method, A, B, C, and D. Because a concurrent dispatch queue is a FIFO queue, block A is dequeued first, followed by blocks B, C, and D. This isn't surprising.

How does a concurrent dispatch queue work?

The difference with a serial dispatch queue is that block B is dequeued before block A has finished executing, block C is dequeued before block B has finished executing, and block D is dequeued before block C has finished executing. We're assuming that the system has plenty of resources to execute blocks A, B, C, and D concurrently.

The order of completion is no longer under the control of Grand Central Dispatch. Which block of work completes first depends on the size and location of the image, the availability of network interfaces, the battery level of the device, and many other factors.

Debugging Concurrent Dispatch Queues

If you want to understand how a concurrent dispatch queue can execute work items concurrently, we need to revisit the Debug Navigator. Enable the breakpoints we set in the loadImage(with:for:) method and run the application. Open the Debug Navigator on the left and make sure the mode is set to View Process by Thread.

Debugging Grand Central Dispatch

The Debug Navigator shows that the block of work is managed by the concurrent dispatch queue we defined in the ViewController class. The block of work is executed on a background thread. The background thread belongs to the pool of threads that is managed by Grand Central Dispatch. Take note of the name of the thread.

Continue execution of the application by clicking the Continue button in the Debug Bar at the bottom. The debugger breaks the application in the loadImage(with:for:) method, but notice that the thread that is executing the block of work isn't the same thread that is executing the first block of work.

Debugging Grand Central Dispatch

The Debug Navigator shows that the concurrent dispatch queue schedules the blocks of work on four different threads. While this may seem surprising, it makes sense if you understand how concurrency works.

Debugging Grand Central Dispatch

A thread is a context in which commands are executed. It's a line or thread of execution through the application we created. This means that a thread can only execute one command at a time. If Grand Central Dispatch were to execute the blocks of work of the concurrent dispatch queue on the same thread, we wouldn't have true concurrency. The blocks of work would be executed serially. That is what happened in the early days of computing when processors had a single core. A core can only execute one command at a time. With a single core, concurrency is nothing more than an optimization in software, not hardware. You can ignore this if it doesn't make sense.

What's important to understand is that Grand Central Dispatch takes advantage of the resources of the system, such as multiple cores, to execute the blocks of work as fast as possible. It does this by scheduling the blocks of work on multiple threads.

Click the Continue button in the Debug Bar until you hit the second breakpoint of the loadImage(with:for:) method. The debugger breaks the application on the main thread and the Debug Navigator shows us that the block of work that is executed was scheduled on a background thread.

Debugging Grand Central Dispatch

If you paid close attention, then you may remember that that thread wasn't the first to start execution of its block of work. That is the power of concurrency. The four blocks of work are executed at the same time and we can't predict which block of work will complete first.

What's Next?

Grand Central Dispatch is a powerful piece of technology and exploring its internal workings is truly fascinating. I hope you're excited to learn more about Grand Central Dispatch because I have a lot more in store for you. In the next episode, we discover the difference between synchronous and asynchronous execution.

Next Episode "Main and Global Dispatch Queues"