Grand Central Dispatch in Swift

Photo of Piotr Sochalewski

Piotr Sochalewski

Updated Feb 20, 2023 • 5 min read
florencia-viadana-723328-unsplash

Grand Central Dispatch is a low-level API created to enable you to execute code concurrently.

It uses dispatch queues managed by the system, which makes it the easiest way to manage operations. Before we dive into GCD, let's have a brief look at the types of execution and queues.

Execution

Work items can be executed synchronously or asynchronously. When executed synchronously with the sync method, the program waits until the execution finishes before the method call returns. When executed asynchronously with the async method, the method call returns immediately.

let queue1 = DispatchQueue(label: "queue1")
let queue2 = DispatchQueue(label: "queue2")

let array1 = ["A", "B", "C"]
let array2 = ["X", "Y", "Z"]

func sync() {
    queue1.sync {
        array1.forEach { print($0) }
    }

    queue2.sync {
        array2.forEach { print($0) }
    }
}

func async() {
    queue1.async {
        array1.forEach { print($0) }
    }

    queue2.async {
        array2.forEach { print($0) }
    }
}

As you can see the code above has two dispatch queues and two arrays of strings (the former contains the three first letters of the alphabet, while the latter the three last letters of alphabet). Both functions print elements of the arrays.

sync() has two synchronous blocks, so the second one is run after the first completes. As you can expect, this prints A B C D E F.

On the other hand, async() has two asynchronous blocks. This means that both blocks can be executed at the same time, so the order of prints is unknown. It may be A X Y B C Z , A X B Y C Z, or A B X C Y Z. You can check it out on your own by running this in Xcode's Playground.

NOTE: You should be very careful with sync(), because it may sometimes result in a deadlock situation. Two (or more) items deadlock if they get stuck waiting for each other to complete. You can see a deadlock if your code looks like this:

let queue = DispatchQueue(label: "queue")
let fruits = ["🍉", "🍌", "🥝"]
let vegetables = ["🥦", "🥕", "🥒"]

queue.async {
    // 1st work item
    fruits.forEach { print($0) }

    queue.sync {
        // 2nd work item
        // 👇 This will never happen
        vegetables.forEach { print($0) }
        
        // Outer closure is waiting for this inner closure to complete
        // Inner closure would not start before outer closure returns
        // Result: deadlock
    }
    
    // 👇 This will never happen
    print("Done")
}

Queues

Queues manage the execution of work items. Each item submitted to a queue is processed on a pool of threads managed by iOS. This means you should not confuse queues with threads. Executing two work items on the same queue, does not mean they are running on the same thread (an exception to this is DispatchQueue.main which always runs on the main thread).

Serial vs concurrent

There are two types of queues: serial and concurrent. Serial is the default one (queues in the code above are serials) and it executes exactly one task at a time. Concurrent allows starting multiple tasks at the same time, but does not guarantee they complete at same time.

The code below behaves pretty much the same as two serial queues with async callbacks from the first example, but it uses just one concurrent queue:

let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent)

func concurrent() {
    concurrentQueue.async {
        array1.forEach { print($0) }
    }

    concurrentQueue.async {
        array2.forEach { print($0) }
    }
}

Again, the result can be different on each run because task completion is not deterministic.

Default queues

Usually, you don't need to create your own queue as iOS offers a few default dispatch queues that are exactly what you need in most cases.

let mainQueue = DispatchQueue.main

There exists a main queue, just one per app. This queue is required for all UI-related things (an app should never update the UI using a background queue), so if a code takes too long to execute on this queue, then the program will appear to be stuck.

That is why you should use global queues. They perform work in the background. A few global queues exist with different QoS (formerly priorities). You can create a global queue this way:

let defaultQueue = DispatchQueue.global()
let backgroundQueue = DispatchQueue.global(qos: .background)

There are six QoSClass' cases: background, utility, default, userInitiated, userInteractive, and unspecified.

A good example of a real world usage case of queues can be any time-consuming operation (such as an API request) that should update the UI as a result. It could look like this:

DispatchQueue.global(qos: .background).async {
    // Execute a time consuming background task
    DispatchQueue.main.async {
        // Update the UI
    }
}

There is more

Grand Central Dispatch is a very useful API for write code in a way that lets its execution to be smooth. The examples above show the most common usages, but if you want to know more, then you should take a look at asyncAfter (not complicated, but useful) and DispatchGroups (more complicated and rarely used, but sometimes very helpful).

Tags

Photo of Piotr Sochalewski

More posts by this author

Piotr Sochalewski

Piotr's programming journey started around 2003 with simple Delphi/Pascal apps. He has loved it...
Efficient software development  Build faster, deliver more  Start now!

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business