As iOS developers we're spoilt when it comes to thread management. The complexity of spinning up and tearing down threads is largely abstracted away into compact APIs like DispatchQueue and NSOperation.

In this article you're going to cover the following topics.

  • What is multithreading and why is it important?
  • How multithreading is handled in the context of Swift-based  software
  • Managing tasks on different threads using DispatchQueue and DispatchGroup

If you already know what multithreading is and why you use it feel free to skip this section, otherwise you're going to start with the basics.

What is Multithreading?

A multithreaded system is an environment where multiple processor cores are available on which to run multiple operations simultaneously. Systems make use of available resource by scheduling work with a different levels of priority on separate threads. Tasks can be interrupted by tasks with a higher priority and resumed when appropriate system resource is available.

Why is it important?

You may be familiar with the main file in your Xcode projects (or @UIApplicationMain in AppDelegate if you created your app to run on iOS13 or newer). This is your application's main runloop, and this always  runs on the main thread. What does it mean exactly?

The main thread is responsible for handling user input and drawing frames (updating your UI) so it has to be responsive. If you give it some thought it's fairly obvious why this is the case. Let's say every time you tapped a button you scheduled a new task to be run on a background thread. Background tasks have a low priority which means the code will execute when there is system resource available and there are no higher priority items to run. It could be instant, it could be 5 seconds, it could be a few minutes - who knows? Imagine if every time you tapped a button in the UI it took an undetermined amount of time to respond, not ideal...

The same goes for scheduling things on the main thread. Let's say you built an app that has to parse some HTML, an operation that could take a while. If you schedule this work on the main thread there would be no resource available to draw frames and handle user input, this would render your app unresponsive. We call this blocking the main thread, and it's never a good thing to do. In fact it's a very common interview topic.

Serial vs Concurrent Queues

When we schedule work using DispatchQueue we get to define the type of queue we want via the attributes parameter. By default the type of queue is serial, we can change the queue type to .concurrent to execute tasks concurrently. The diagram below illustrates the differences between these two approaches.

We'll start with the Serial Queue. Tasks are executed in the same order in which they arrive, and the next task is not started until the previous one is finished. This is called a FIFO queue (first in first out). This kind of queue is useful to avoid access conflicts, where two processes may need to update the same resource. A Serial queue would guarantee that only one process access the data at once, and in the correct order. When using DispatchQueue it is serial by default.

Concurrent queues allow processes to run concurrently, or at the same time. The tasks are executed in the order they come in but can finish at different times. Concurrent queues cannot guarantee which thread a particular task is performed on, as it may offload work to separate threads. This is particularly useful when you have tasks that take a long time as we covered above.

Concurrency using DispatchQueue

DispatchQueue is belongs to the Dispatch framework (also known as Grand Central Dispatch, or GCD). This API alone is enough for a lot of engineers to satisfy all their multithreading requirements. It's surprisingly easy to create your own queue's and schedule work, or you can use the global queue provided by the framework.

Open a new playground and add the following code. Make sure you open the playground in Xcode - not the playgrounds app - so you can see the print statements in the console.

let queue = DispatchQueue(label: "com.mytask", attributes: .concurrent)
queue.async {
    print("1")
    print("2")
    print("3")
}

queue.async {
    print("4")
    print("5")
    print("6")
}

This is pretty simple, you've created a new DispatchQueue object with a label and marked it as .concurrent. Next you perform tasks on the queue using queue.async. You may expect the console output to look like the following.

1
2
3
4
5
6

You'll notice this isn't the case, the actual output will vary each time you run, but it will look something like.

1
4
5
2
6
3

That's because you marked the queue as concurrent! The print() calls are executed with no guaranteed order.

Change the declaration of queue to the following.

let queue = DispatchQueue(label: "com.mytask")

Run again and you'll see the following.

1
2
3
4
5
6

The default queue type when using DispatchQueue is serial. Now you see the numbers printed in the order you expect.

That's a very basic yet powerful example of the differences between serial and concurrent queues, and a brief introduction into the syntax for the DispatchQueue API.

In the above example you created your own queue, but too many custom queues can cause issues with system thread allocation. To combat this, Apple recommends you use the global queue. You can get the global queue with a specified quality of service (more on that next) and perform tasks with the following code.

DispatchQueue.global(qos: .background).async {
    // Task
}

// Ommit qos parameter for .default qos 
DispatchQueue.global().async {
    // Task
}

DispatchQos.QoSClass is an enum that describes the intent of a queue, the system uses the intent to distribute tasks between available resources. The available options for quality of service are:

  • userInteractiveThe quality-of-service class for user-interactive tasks, such as animations, event handling, or updating your app's user interface.
  • userInitiatedThe quality-of-service class for tasks that prevent the user from actively using your app.
  • default The default quality-of-service class.
  • utilityThe quality-of-service class for tasks that the user does not track actively.
  • backgroundThe quality-of-service class for maintenance or cleanup tasks that you create.
  • unspecifiedThe absence of a quality-of-service class.

Source: Apple docs

Next you'll take a look at how this works in practice. Using the highest priority quality of service and the lowest, take a look at the following code.

DispatchQueue.global(qos: .background).async {
    print("completed task from background")
}

DispatchQueue.global(qos: .userInteractive).async {
    print("completed task on main thread")
}

By now you'll likely know what's coming, run this code and take a look at the console.

completed task on main thread
completed task from background

The task with the userInteractive qos finished first, even though it was scheduled after the task with the background qos, that's because the queue with the userInteractive qos takes higher priority.

Updating the main thread

Earlier in this post we discussed the main thread's role in your app, but how and when is it appropriate to update it? It's fairly simple, whenever we perform work on a background thread that causes your app's UI to change, the process of updating the UI should be performed on the main thread. Modern Xcode apps will give you a warning if this isn't the case.

DispatchQueue makes updating the main thread is simple, take a look at the following code.

DispatchQueue.main.async {
    // Perform task
}

You can place that code inside a call that happens in a background queue, like the following.

DispatchQueue.global(qos: .background).async {
    // Background task goes here
    
    DispatchQueue.main.async {
        // Main thread task goes here
    }
}

Next you'll take a look at a real world example where performing work on the main thread is necessary. In the following example you'll create a small app that displays random Chuck Norris jokes from an API. Add the following code to a playground.  

import Foundation
import PlaygroundSupport
import UIKit

class TableViewController: UITableViewController {

    var jokes = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.rowHeight = UITableView.automaticDimension
        tableView.delegate = self
        tableView.dataSource = self

        self.getJokes()
    }

    func getJokes() {
        let request = URLSession.shared.dataTask(with: 
        URL(string: "http://api.icndb.com/jokes/random/10")!) {
        (data, response, error) in
            if
                let data = data,
                let json = try? JSONSerialization
                    .jsonObject(with: data, options: .allowFragments) 
                    as? [String: Any],
                let values = json["value"] as? [[String: Any]] {
                
                self.jokes = values.compactMap { $0["joke"] as? String }
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }
        }
        request.resume()
    }
}

extension TableViewController {
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView,
    	numberOfRowsInSection section: Int) -> Int {
        return self.jokes.count
    }

    override func tableView(_ tableView: UITableView, 
    	cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.numberOfLines = 0
        cell.textLabel?.text = self.jokes[indexPath.row]
        return cell
    }
}

PlaygroundPage.current.liveView = TableViewController()

When you run this in s playground you'll see a list of jokes appear in a table view in the playground live view.

Firstly take a look at the  getJokes() function. You'll see that you use URLSession to make a network call, but the network call isn't offloaded to a background thread, this may seem strange given what you've covered so far. You don't need to move this task to the background as URLSession already does its work on a background thread.

In the same function find the call to the main thread. Take a look at what's happening here.

  • You're making a network call on a background thread (URLSession does this automatically)
  • Parsing the incoming data
  • Updating the table view on the main thread

If you didn't move work to the main thread in the final step the UI would attempt to update in the background and the app wouldn't feel responsive. By moving the table reload to the main thread you guarantee it will happen immediately.

Sync vs Async

There's one important detail we've skimmed over, that's the difference between doing something synchronously and asynchronously. In each of the examples so far you've used .async, but you could also use .sync. You'll likely use the former more, but it's important to know the difference between them.

Revisit the first example and add an extra print statement after the async tasks.

let queue = DispatchQueue(label: "com.mytask", attributes: .concurrent)
queue.async {
    print("1")
    print("2")
    print("3")
}

queue.async {
    print("4")
    print("5")
    print("6")
}

print("We can continue")

Running this you'll see something like the following.

1
We can continue
4
2
5
3
6

Now change both queue.async calls to queue.sync and run the example again. You'll see the following output.

1
2
3
4
5
6
We can continue

These differences are important to understand. Running work with async will return immediately and the work inside the closure will continue on another thread. Whereas sync will not return until all work on the queue is finished. With that in mind, calling something async will never block the thread you are calling from, but using sync can, so you need to be careful!

Grouping processes with DispatchGroup

I've found DispatchGroups incredibly useful. It's a fairly simple concept really; if you have more than one task you want to run, and you want to know when all those tasks are complete (call a completion block after a few network calls for example) you can use put all those processes inside a DispatchGroup.

Again you'll edit the first example to illustrate this.

let queue = DispatchQueue(label: "com.mytask")
let group = DispatchGroup()
group.notify(queue: .main) {
    print("Completed all tasks")
}

group.enter()
queue.async {
    print("1")
    print("2")
    print("3")
    group.leave()
}

group.enter()
queue.async {
    print("4")
    print("5")
    print("6")
    group.leave()
}

The syntax for a DispatchGroup is relatively straightforward.

  • Create a DispatchGroup object
  • Set the group to notify you of completion on the main queue, and provide a closure to execute when completed.
  • When you want to add code to a group, you simply call group.enter()
  • When the task in a specific work item is done you call group.leave()

Run this example and you'll see the following output.

1
2
3
4
5
6
Completed all tasks

It's important to note that work items inside a DispatchGroup don't need to be on the same queue.

I've personally found the DispatchQueue very useful, as i've previously worked on apps that require a lot of front loading of data.


That's it for part one of this two-part series.

In this post you've covered:

  • Differences between concurrent and serial queues
  • Scheduling work on a DispatchQueue
  • Prioritising work items using quality of service
  • Updating the main thread
  • Differences between sync and async
  • Scheduling multiple work items with DispatchGroup

In part two of this series you'll take a look at NSOperationQueue.

Thanks for reading ✌️