Better Programming

Advice for programmers.

Follow publication

A Guide to Concurrency in iOS and Swift

Comparison of DispatchQueue, Operation and OperationQueues, and Swift Concurrency

Anurag Ajwani
Better Programming
Published in
10 min readJan 12, 2022

Photo by Jonathan Chng on Unsplash

Does your app suffer from the user interface being frozen time to time? Does your app gets killed by the system because your app is unresponsive?

Apps that suffer from the conditions above can be due to your app doing a task that is heavy and/or long in the same thread which is in charge of responding to the user interface interactions.

In this post, I will cover how you can solve these problems. This post is not a tutorial. I have covered most of the content herein separate posts with tutorials which I will reference along the way.

Why does your app freeze?

Let’s say you are fetching images from a web server which can take a few seconds. Whilst your app’s process is waiting to receive the image it won’t respond to the user interactions.

The user might feel the app “unresponsive” for a few seconds before their taps are processed.

What are processes and threads?

Let’s say the user is on the home screen. The user taps on your app. This tells iOS to launch your app. In this case, iOS will create a new process for your app and create a thread with it.

A process is an instance of a program–in this case your app. In iOS you can only run one instance of your app.

Other operating systems may allow running multiple instances of an app. For each process or instance, the operating system will load your program from disk into memory.

Each app process in iOS is launched with one thread. This is known as the main thread or the UI thread. Other threads can be created by your app or program. Threads share the same memory space.

A thread is a sequence of instructions. Your program sends instructions to the thread. These are queued and executed first come first serve or more commonly known as First In First Out (FIFO).

How to solve freezing app problems?

The solution to freezing app problems is to offload heavy or long-running tasks into new threads. By offloading the task into a new thread frees up the UI thread to keep responding to the user's interaction with the UI.

The act of delegating a task and continue doing your own tasks until the delegated task returns with a result is known as performing the task asynchronously. We are not waiting blocked until the task is performed. When waiting for task to be performed before continuing with our own is known as synchronous.

So how can we execute code on other threads in Swift and iOS? Here are the iOS and Swift native possibilities:

  1. Grand Central Dispatch (GCD)
  2. Operation and OperationQueues
  3. Swift Concurrency

GCD has been around for long while in iOS. Operation and OperationQueues build on top of GCD and simplify some of its shortcomings as we’ll see in that section.

Swift Concurrency was introduced recently and simplifies performing concurrent tasks. You don’t need to know GCD and, Operation and OperationQueues. However, Swift concurrency is only supported from iOS 13 or newer. Xcode 13.2 or newer is also required.

Please note the aim of this post is to understand the different options and compare them.

How to use Grand Central Dispatch

The main way to use Grand Central Dispatch is through its API interface DispatchQueue. A DispatchQueue is not a thread. It is simply a queue. Tasks are added to the queue. Then GCD will pick the oldest task and execute that first.

Basically it's first come first serve basis; technically known as First In First Out or FIFO. Threads get associated to queues by the system as they are available.

There are two types of queues:

  1. Serial Queues
  2. Concurrent Queues

There is also a default dispatch queue for the user interface interaction that performs all of its tasks on the main/UI thread. This thread is known as the main queue (DispatchQueue.main) and it's provided at the app launch. The main queue type is serial.

Serial Queue

Serial Queues are queues where tasks are executed one after the other and only when the previous task has been completed.

To create a serial queue:

let queue = DispatchQueue(label: "my_serial_queue")

By default when you create a queue its configured for serial execution.

Task executed when previous task is completed

Concurrent Queue

Concurrent Queues executes tasks one after the other without waiting on the previous task to complete. It does not mean that tasks will be all executed in one go. The execution of tasks will be based on system resources or available threads from the thread pool.

To create a concurrent queue:

let queue = DispatchQueue(label: "my_concurrent_queue", attributes: .concurrent)
Multiple tasks are being executed concurrently

How to perform tasks on DispatchQueues

To push a task to perform on dispatch queues we need to include the code to be executed in a closure.

let taskToPerform = {
// do something
}

DispatchQueues allows us to perform tasks async and sync.

As mentioned in the previous section async or asynchronous will place the task to perform into the queue and continue executing the next piece of code.

let taskToPerform = {
print("2. This will print second")
// do something
}
queue.async(execute: taskToPerform)print("1. This will print first")

However, sync or synchronous will place the task in the queue and wait till the task is performed before continuing executing the next line of code.

let taskToPerform = {
print("1. This will print first")
// do something
}
queue.sync(execute: taskToPerform)print("2. This will print second")

How to update the UI with the results of tasks executed in a non-main queue

All UI updates must be executed on the main queue. So how can you execute a UI update from a task working in another queue?

How to execute tasks on the UI thread?

From your non-main queue, you can call the main queue by using DispatchQueue.main:

let taskToPerform = {
// do something
DispatchQueue.main.async {
// update UI
}
}
mySerialQueue.async(execute: taskToPerform)

Any UI updates from background (non-main) queues can fail and result in unexpected behavior. Xcode includes and enables a “Main Thread Checker” which will check UI updates from background threads and throw an exception when debugging.

Runtime API Checking “Main Thread Checker” option enabled

How to perform multiple async tasks in order

What if we need to execute asynchronous tasks in order? Let’s say we have a salad to prepare which needs to be executed in the following order:

  1. Add lettuce
  2. Add tomatoes
  3. Add red onion
  4. Add sweetcorn
  5. Add tuna with sunflower oil

With DispatchQueue’s it would look something like the following:

queue.async {
// prepare lettuce
queue.async {
// prepare tomatoes
queue.async {
// prepare red onion
queue.async {
// prepare sweetcorn
queue.async {
// prepare tuna
// completed salad!
}
}
}
}
}

Above tomatoes preparation is dependant on the completion of lettuce. Red onion is dependant on the preparation of tomatoes and so on.

As we add more ingredients to the salad our code can get quite nested. This problem is also known as “pyramid of doom”. The code is quite hard to follow and thus to change too.

Executing tasks concurrently and get completion using DispatchGroup

What if we want to execute multiple tasks at the same time? What if we want to prepare our salad from the previous section without caring about the order of preparation?

We can just prepare all ingredients asynchronously on a concurrent queue:

concurrentQueue.async { /* prepare lettuce */ }
concurrentQueue.async { /* prepare tomatoes */ }
concurrentQueue.async { /* prepare red onion */ }
concurrentQueue.async { /* prepare sweetcorn */ }
concurrentQueue.async { /* prepare tuna */ }

However, how can we get notified when all of the ingredients have been prepared to serve the bowl of salad? By using DispatchGroup.

DispatchGroup is a tool within the Dispatch framework that allows us to get notified when all specified tasks have all completed execution. Here is an example of how to use DispatchGroup:

let dispatchGroup = DispatchGroup()dispatchGroup.enter()
concurrentQueue.async {
/* prepare lettuce */
dispatchGroup.leave()
}
dispatchGroup.enter()
concurrentQueue.async {
/* prepare tomatoes */
dispatchGroup.leave()
}
... // enter and leave for each ingredientdispatchGroup.notify(queue: .main, execute: { /* serve salad bowl */ })

You can learn more on DispatchGroup in my blog post:

How to use Operation and Operation Queues

An Operation is simply a task to perform. OperationQueues are akin to DispatchQueues.

let taskToPerform = BlockOperation {
// do something
}
let operationQueue = OperationQueue()
operationQueue.addOperation(taskToPerform)

So why have and use OperationQueue’s? They have several advantages. However, for the scope of this post is worth highlighting that they allow managing dependencies between tasks to perform:

let prepareLettuceTask = BlockOperation {
// prepare lettuce
}
let prepareTomatoesTask = BlockOperation {
// prepare tomatoes
}
prepareTomatoesTask.addDependency(prepareLettuceTask)let operationQueue = OperationQueue()
let operationsToPerform = [prepareLettuceTask, prepareTomatoesTask]
operationQueue.addOperations(operationsToPerform, waitUntilFinished: false)

You can also perform operations concurrently:

let prepareLettuceTask = BlockOperation {
// prepare lettuce
}
let prepareTomatoesTask = BlockOperation {
// prepare tomatoes
}
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2
let operationsToPerform = [prepareLettuceTask, prepareTomatoesTask]
operationQueue.addOperations(operationsToPerform, waitUntilFinished: false)

Note above we have specified the maximum number of concurrent operations. When not specified it uses a default value which will vary based on the system's available resources. Also, note the code is very similar to the previous example with the exception of no dependency declared.

Additionally, it’s easier to write and manage more complex operations. Let’s say you want to prepare lettuce and tomatoes concurrently. However, you want prepare the red onions only when those two operations have been completed:

let prepareLettuceTask = BlockOperation {
// prepare lettuce
}
let prepareTomatoesTask = BlockOperation {
// prepare tomatoes
}
let prepareRedOnionTask = BlockOperation {
// prepare red onions
}
prepareRedOnionTask.addDependency(prepareLettuceTask)
prepareRedOnionTask.addDependency(prepareTomatoesTask)
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 2
let operationsToPerform = [prepareLettuceTask, prepareTomatoesTask, prepareRedOnionTask]
operationQueue.addOperations(operationsToPerform, waitUntilFinished: false)

In the scenario above the queue will execute lettuce and tomatoes preparation in parallel or concurrently. Once those two operations have been completed then the queue will execute the red onion preparation.

How to use Swift Concurrency

Swift concurrency features have been recently released. These features are available from Swift 5.5 and can be used from iOS 13.

Swift Concurrency features aim to fix multiple issues with the current ways of doing concurrency with DispatchQueue’s and, Operation and OperationQueue’s. We’ll uncover some of these through examples.

Let’s go back to our salad-making example. Let’s say we have a SaladMaker class with one make function:

class SaladMaker {
func make(onCompletion completionHandler: () -> Void) {
...
}
}

Let’s say we need to make our salad in order. Here’s an example using DispatchQueue’s:

class SaladMaker {
func make(onCompletion completionHandler: () -> Void) {
queue.async {
// prepare lettuce
queue.async {
// prepare tomatoes
queue.async {
// prepare red onion
queue.async {
// prepare sweetcorn
queue.async {
// prepare tuna
completionHandler()
}
}
}
}
}
}
}

There are two main problems here worth highlighting:

  1. Pyramid of doom (nested closures)
  2. You can easily forget to call the completionHandler and not notice

With Swift Concurrency the code above will look like the following:

struct Salad {
func addIngredient(_ ingredient: Ingredient) {
...
}
}
class SaladMaker {
func make() async -> Salad {
let salad = Salad()
let lettuce = await self.prepareLettuce()
salad.addIngredient(lettuce)
let tomatoes = await self.prepareTomatoes()
salad.addIngredient(tomatoes)
let redOnion = await self.prepareRedOnion()
salad.addIngredient(redOnion)
let sweetcorn = await self.prepareSweetcorn()
salad.addIngredient(sweetcorn)
let tuna = await self.prepareTuna()
salad.addIngredient(tuna)
return salad
}
private func prepareLettuce() async -> Ingredient {
...
}
private func prepareTomatoes() async -> Ingredient {
...
}
private func prepareRedOnion() async -> Ingredient {
...
}
private func prepareSweetcorn() async -> Ingredient {
...
}
private func prepareTuna() async -> Ingredient {
...
}
}

The code above looks closer to our synchronous code. Readability is improved. Additionally, Swift compiler is able to check that values are returned.

Not only does Swift concurrency makes it easier to code and debug async code but it’s also abstracted from the platform. DispatchQueue’s and, Operation and OperationQueue’s are platform dependant. Thus with Swift Concurrency it becomes possible and easier to port your Swift code from one platform to another (i.e. Linux).

To learn more on Swift Concurrency through a tutorial you can read my blog post:

Final Thoughts

Concurrency in iOS and Swift haven’t seen major changes in many years.

I welcome the new Swift Concurrency features added into Swift 5.5. Swift Concurrency is most surely the way to go.

However, the caveat of providing support only on iOS 13 or newer makes these features not yet available for the current product I’m maintaining and working on.

I am looking forward to the day I can implement these features on the projects I am working on!

Want to Connect With the Author?For more on iOS development follow me on Twitter

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Anurag Ajwani
Anurag Ajwani

No responses yet

Write a response