Concurrency

Introduction to Concurrency in iOS using Grand Central Dispatch (GCD)

Learn how to perform multiple task concurrently in iOS

Anurag Ajwani

--

Photo by Braden Collum on Unsplash

How do you execute multiple pieces of code at the same time? When beginning coding we are taught code that executes serially or one after the other in order.

Let’s look at an example. Let’s say you’re making a simple salad. Here are the ingredients:

  • lettuce
  • tomatoes
  • red onion
  • sweetcorn
  • tuna with sunflower oil

The process is simple: cut what needs cutting and mix it all.

addLettuce()
addTomatoes()
addRedOnion()
addSweetcorn()
addTuna()
mix()

This simple salad does not has a specific order in which to add the ingredients. If we had 5 people each one could do one task all 5 tasks could be performed at the same time (concurrently).

If each task took 1 minute to do on its own the salad would take 5 minutes to make. However if all tasks were done concurrently the salad would take only 1 minute to make.

So it’s clear learning how to do tasks concurrently is important to save us time. However there is one more reason more important as to why you should learn how to perform tasks concurrently on iOS than just saving time.

In this post I’ll teach why learning concurrency for iOS development is important. Then we’ll look at an example oh how to execute code concurrently on iOS. Finally we’ll explore concurrency in an app that will make our salad.

For this post I assume you are already familiar with the basics of iOS app development and Swift programming language.

For this post I have used Xcode 12.2.

Why learn concurrency on iOS?

In iOS your app user interface(the views the user interacts with) must always be responsive. That is the user must be always be able to interact with it. If a user taps on some button on the screen and nothing happens the user will assume the app is frozen. This is really bad user experience. Furthermore the iOS operating system will kill your app after a period of unresponsiveness. But what has responsiveness to do with concurrency?

Let’s go back to the salad example. Now say you are running a salad restaurant and you have no employees to help you out. You wait on the till until a customer arrives. The customer places an order for a salad and you make the salad.

When a customer visits your restaurant they will interact with you. You are the customer interface–or user interface in software–in the restaurant. That is how the customer interacts with your business. They talk to you. They make their order to you.

When you receive the order you go back and start making this order. Whilst you are making the salad a second customer arrives. However after seeing no one in the till and asking if anyone is around the customer leaves as there was no response. The business is unresponsive. The potential customer has a bad customer experience and decides to not return. This will kill your business.

Learning concurrency is like being able to employ other people–or workers–to perform tasks as and when needed in your restaurant. You’ll never have to leave the till. You’ll always be available to the customer.

How to perform tasks concurrently on iOS?

In iOS we can perform multiple tasks concurrently using a framework called Dispatch–also known as Grand Central Dispatch and in short referred by its initials GCD.

GCD provides us with the tools to tell a worker–more specifically called queues–to perform specific tasks. There are many ways offered by Dispatch framework to manage workers. In this post we’ll cover only two of the most commonly used ways:

  1. Creating an instance of a worker and tell it to perform the tasks
  2. Tell GCD the task to perform and let it decide to which worker to assign it to

1. Creating an instance of a worker and tell it to perform the tasks

A worker is known as a serial queue in GCD terms. These perform task first-in-first-out (FIFO). The worker or serial queue will not execute the following task until the task at hand is performed.

To create a worker:

let worker1 = DispatchQueue(label: "worker1")

Now we can tell the worker to perform a task. However we can tell the worker to the job and we can go each our own way(asynchronously) or we can wait for the worker to finish their task(synchronous). To tell it to do things:

func performTaskAsynchronous() {
worker1.async { // 1
// do something that takes 2 seconds
print("this will print after") // 3
}
print("this will print before") // 2
}
func performTaskSynchronous() {
worker1.sync { // 1
// do something that takes 2 seconds
print("this will print before") // 2
}
print("this will print after") // 3
}

Notice in the example above performTaskSynchronous will only perform the statement after worker1.sync has finished. However performTaskAsynchronous will execute worker1.async and perform the next statement immediately without waiting for worker1 to finish its task.

We’ll be making use of async mainly in this post.

2. Tell GCD the task to perform and let it decide to which worker to assign it to

Let’s say again you are employing a worker for each ingredient. If you are managing your own workers you’ll need to add a new worker for each ingredient. Each worker is tied to one ingredient. No worker performs two task. Wouldn’t it be convenient if we could create a new worker and tell it to perform the task all in one call? Well the Dispatch framework already offers that by using function named global. Let’s look at how this works:

DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async { 
// do something
}

Note that we have to provide the function a quality of service value. This tells the framework how to prioritise the execution of the code. I won’t delve too much prioritisation. However do consider how important is the execution of the code to the user at this time. In this case the user is waiting for their salad to be produced at the request of the user, thus we provided the .userInitiated priority.

DispatchQueue allocating tasks to other queues

How to use Grand Central Dispatch to perform tasks concurrently

In this section we’ll go hands-on using DispatchQueues from the Dispatch framework. We’ll explore how to make a simple salad in software first by doing everything on the main thread, then through a single worker serial queue and finally using the global convenience function to prepare all ingredients in parallel. We’ll be starting from an already existing app. The app contains classes to prepare each individual ingredient. Once the ingredient is ready to be mixed we’ll add it to the bowl of salad. On each ingredient mixed we’ll update the UI so the user can be updated with the progress of their salad.

The steps we’ll take:

  1. Download starter pack
  2. Preparing the salad on a single worker
  3. Preparing each ingredient on an individual worker

Let’s get started.

1. Download starter pack

Let’s start by downloading the starter pack. Open Terminal app and execute the following commands:

cd $HOME
curl https://github.com/anuragajwani/SaladMaker/archive/starter.zip -o salad_maker.zip -L -s
unzip -q salad_maker.zip
cd SaladMaker-starter
open -a Xcode SaladMaker.xcodeproj

In this post we’ll mostly focusing on the SaladMaker class in SaladMaker.swift file.

2. Preparing the salad on a single worker

Open SaladMaker in SaladMaker.swift file in Xcode. Currently the file makes the salad ingredients one after the other. I have added a one second delay to the prep time by using sleep. If we increase the sleep time we increase the chances of iOS killing the app. Run the app.

Note the app shows all ingredients preparing or ready at the same time. If you inspect the code we update the UI after each ingredient is ready. However the UI doesn’t update until all ingredients are ready. Why? Because the worker that is suppose to update the UI is busy preparing the next ingredient.

Queue enable to update UI due to other tasks in queue

I have added a 1.5 second delay before starting prepping the salad. However notice that after 1.5 second if you tap on the “Cancel” button the screen doesn’t change immediately. It can take a long while before the UI updates. Why? Again the main worker is busy first prepping all the ingredients, then updating the UI then executing any new commands that was instructed during the salad preparation period.

New commands whilst the worker is busy gets appeneded to the end of the queue

Let’s fix this problem by employing a worker to perform the preparation of the salad. Open SaladMaker and change the make function implementation to the following:

func make(onIngredientPrepped: @escaping (Ingredient) -> ()) {
let worker1 = DispatchQueue(label: "salad_maker_queue")
Ingredient.allCases.forEach { (ingredient) in
worker1.async {
let randomPrepTime = UInt32.random(in: 1...3)
sleep(randomPrepTime)
DispatchQueue.main.async {
onIngredientPrepped(ingredient)
}
}
}
}

Above we have a worker that now prepares each ingredient. Additionally after finishing each ingredient the worker notifies the completion to the UI thread.

Employing a worker to prepare the salad — Completion scenario
Employing a worker to prepare the salad — Cancel scenario

Run the app and notice now all of the ingredients are being prepared one after the other and the UI is being updated promptly.

3. Offloading salad preparation to another worker

In the previous step we employed a single worker to prepare each ingredient. The ingredients preparation executed in order. First lettuce, then onions and so forth. However this salad has no order of preparation. If we could employ one worker to do each ingredient we could save up-to 80% of time in preparation!

We could employ one worker per ingredient. Alternatively we could use the convenience function that Dispatch framework offers and let it allocate the work to a worker of its choice. Let’s do the latter for simplicity. Open SaladMaker and change the make function to the following:

func make(onIngredientPrepped: @escaping (Ingredient) -> ()) {
Ingredient.allCases.forEach { (ingredient) in
DispatchQueue.global(qos: .userInitiated).async {
let randomPrepTime = UInt32.random(in: 1...3)
sleep(randomPrepTime)
DispatchQueue.main.async {
onIngredientPrepped(ingredient)
}
}
}
}

In the code above we have removed the creation of a worker or a queue. Then we have changed worker1.async { ... }for DispatchQueue.global(qos: .userInitiated).async { ... }. Simply we are telling the Dispatch framework to allocate the work for us. The Dispatch framework will then decide–based on system resources–the amount of workers to employ. Most likely it will employ 5 workers each to do each ingredient.

Dispatch allocating each ingredient prep to an individual worker

Run the app and watch it complete faster than ever!

Summary

In this post we learnt:

  • What is concurrency
  • Why learn about concurrency on iOS
  • How to perform concurrent tasks on iOS

Final notes

You can find the full source code on Github:

Mastering concurrency can take some time so take it easy on yourself and give yourself the chance to digest this.

In the example used in this post we explored making a salad that required no specific order to prepare. However how managed the salad completion status after keeping track of each ingredient within the bowl. If you were to add another ingredient you’d also have to account for the status of such ingredient. However is there a more elegant and scalable way of managing the completion of a series of asynchronous tasks? Yes there is and I have covered that topic in my post “How to perform parallel asynchronous operations with DispatchGroup”.

For more on iOS development follow me on Twitter or Medium!

--

--

Anurag Ajwani

Senior iOS Engineer at Travelperk. 7+ years experience with iOS and Swift. Blogging my knowledge and experience.