Mastering Grand Central Dispatch

Synchronous and Asynchronous Execution

Stop Writing Swift That Sucks

DISCLAIMER: No Rocket Science Involved

Join 20,000+ Developers Learning About Swift Development

Download the 4 Swift Patterns I Swear By

Up until now we submitted blocks of work to a dispatch queue by invoking the async(execute:) method. In this episode, we explore how Grand Central Dispatch handles the execution of a block of work. Work can be executed synchronously or asynchronously. What does that mean? What is the difference? And what are the risks?

Asynchronous Execution

I'm going to use a playground to illustrate the differences between the synchronous and asynchronous execution of a block of work. Open Xcode and create a playground by choosing the Blank template from the list of iOS > Playground templates.

Creating A Playground in Xcode

Remove the contents of the playground and add an import statement for the Foundation framework at the top.

import Foundation

We start by asking the DispatchQueue class for a reference to a global dispatch queue. We store the reference in a constant, dispatchQueue. The quality of service class of the dispatch queue isn't relevant for this discussion.

let dispatchQueue = DispatchQueue.global()

Let's submit a block of work to the dispatch queue by invoking the async(execute:) method. In the closure that we pass to the async(execute:) method, we fetch the data for a remote resource, an image. We print the number of bytes stored in the Data instance.

dispatchQueue.async {
    let data = try! Data(contentsOf: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg")!)
    print(data.count)
}

Notice that I use the exclamation mark and the try! keyword. This episode is focused on understanding the differences between the synchronous and asynchronous execution of a block of work. Safety isn't a priority.

To show you the difference between synchronous and asynchronous execution, we add a print statement before and after the invocation of the async(execute:) method.

import Foundation

let dispatchQueue = DispatchQueue.global()

print("before")

dispatchQueue.async {
    let data = try! Data(contentsOf: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg")!)
    print(data.count)
}

print("after")

Execute the contents of the playground to see the result. Open the console at the bottom and inspect the output. The output shouldn't surprise you. Fetching the data of the image is dispatched to a background thread, which means that the main thread isn't blocked. The output in the console confirms this. The print statements before and after the invocation of the async(execute:) method are executed before the size of the Data instance is printed to the console.

before
after
2328941

What does that say about the async(execute:) method? The async(execute:) method returns immediately. It submits the block of work to the global dispatch queue and returns control to the thread from which the async(execute:) method is invoked. This isn't new and it's a proven recipe for keeping an application performant and responsive.

Let me visualize this to better understand what is happening. After the first print statement is executed on the main thread, the block of work that fetches the data for the remote resource is submitted to a global dispatch queue. The async(execute:) method returns immediately. It doesn't wait for the block of work to start its execution. The async(execute:) method returns as soon as the block of work has been submitted to the global dispatch queue. This means that the second print statement is executed on the main thread immediately after invoking the async(execute:) method. Grand Central Dispatch decides when it's most opportune to execute the block of work on a background thread. The size of the remote resource is printed to the console after the data for the remote resource has been downloaded.

Asynchronous Execution

Synchronous Execution

It's also possible to execute the block of work synchronously by passing the block of work to the sync(execute:) method. Let's try it out. Replace the async(execute:) method with the sync(execute:) method and execute the contents of the playground to see the difference.

import Foundation

let dispatchQueue = DispatchQueue.global()

print("before")

dispatchQueue.sync {
    let data = try! Data(contentsOf: URL(string: "https://cdn.cocoacasts.com/7ba5c3e7df669703cd7f0f0d4cefa5e5947126a8/1.jpg")!)
    print(data.count)
}

print("after")

The output in the console clearly highlights the differences between the async(execute:) method and the sync(execute:) method.

before
2328941
after

The sync(execute:) method doesn't immediately return control to the thread from which the sync(execute:) method is invoked. In other words, the sync(execute:) method blocks the thread from which it is invoked, that is, the calling thread. Control is returned after the block of work has finished executing. This is better known as synchronous execution.

Let me illustrate what happens. The main thread executes the first print statement. It then dispatches a block of work to a global dispatch queue for synchronous execution. Because the block of work is executed synchronously, the calling thread, the main thread in this example, is blocked until the block of work has finished executing. The sync(execute:) method returns control to the main thread after downloading the data for the remote resource and printing the size of the Data instance to the console. The last print statement is executed immediately after the sync(execute:) method returns control to the main thread. That explains the output we see in the console.

Synchronous Execution

You may be wondering why you ever would want a block of work to be executed synchronously. Isn't the benefit of using Grand Central Dispatch that work can be dispatched to a background thread for asynchronous execution? That is true, but it can be useful in some scenarios. Let me give you an example.

The sync(execute:) method is useful to serialize state between subsystems. To understand what that means, you first need to become familiar with mutual exclusion. What is mutual exclusion and why is it important?

Grand Central Dispatch makes it easy to take advantage of the capabilities of a modern device. You don't need to worry about threads or the resources that are available. But, as the saying goes, with great power comes great responsibility. It's easy to pass data from one thread to another using Grand Central Dispatch. But that can lead to problems when a single resource is accessed and mutated from multiple threads. This can result in race conditions and hard to find bugs. Threading issues are often difficult to find and debug.

Mutual exclusion, or mutex for short, is a construct that prevents multiple objects from accessing a shared resource at the same time. That is where synchronous execution comes into play.

You can pass a shared resource to a subsystem by handing it to a dispatch queue and executing the block of work synchronously. How is that helpful? Because the block of work is executed synchronously, it blocks the thread from which the sync(execute:) method was invoked. In other words, the thread that passed the resource to the subsystem won't and can't access the shared resource as long as the sync(execute:) method hasn't returned control to the calling thread.

You can accomplish the same result by using locks, but locks are notoriously hard to manage and they add a substantial amount of complexity.

I hope it's clear that you use the sync(execute:) method in a few specialized scenarios. Most of the time, you'll find yourself using the async(execute:) method.

Optimizations

Before we move on, I'd like to show you an interesting implementation detail of synchronous execution. Let's revisit the project from the previous episode. Open ViewController.swift and navigate to the viewDidLoad() method. The view controller asynchronously executes the block of work that is passed to the global dispatch queue. Let's replace the async(execute:) method with the sync(execute:) method. Enable the first breakpoint of the loadImage(with:for:) method and run the application.

override func viewDidLoad() {
    super.viewDidLoad()

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

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

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

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

The output in the Debug Navigator may surprise you. The view controller dispatches the fetching of the data to a global dispatch queue. You would think that that means the fetching of the data is executed on a background thread. That doesn't seem to be true. The Debug Navigator shows us that the fetching of the data takes place on the main thread. How is that possible?

The Debug Navigator shows that the fetching of the data takes place on the main thread.

Remember that Grand Central Dispatch doesn't make a guarantee as to which thread is used to execute a block of work. Grand Central Dispatch is quite smart and carefully considers which thread should execute a block of work. It knows that the block of work that is passed to the global dispatch queue is executed synchronously. This means that the calling thread, the main thread, is blocked until the block of work has finished executing.

You don't need to be a rocket scientist to figure out that there's no need to dispatch the execution of the block of work to a background thread. It makes more sense to execute the block of work on the main thread because it is waiting until the block of work has finished executing anyway. Executing the block of work on a background thread also comes with a bit of overhead and Grand Central Dispatch knows that. It optimizes the execution of the block of work by executing it on the main thread.

This example illustrates why Grand Central Dispatch is such a powerful technology. It is more than an API for dispatching work to a background thread. The example also confirms that the main thread isn't strictly bound to the main dispatch queue. Grand Central Dispatch decides when it's appropriate to execute a block of work on the main thread.

What's Next?

Understanding the difference between synchronous and asynchronous execution is important if you work with Grand Central Dispatch. You won't be using the sync(execute:) method often, but it certainly has its uses. Later in this series, we revisit synchronous execution when we discuss deadlocks, serialization, and synchronization.

Stop Writing Swift That Sucks

DISCLAIMER: No Rocket Science Involved

Join 20,000+ Developers Learning About Swift Development

Download the 4 Swift Patterns I Swear By
Next Episode "Adding Flexibility With Dispatch Work Items"