iOS Concurrency

Concurrency is very important when you want to do a lot of tasks at the same time in your app. There are a lot of ways that can be used for better speed, performance, and overall responsiveness. One of these ways is concurrency.

Mahmoud Elshakoushy
7 min readJan 31, 2021

What is concurrency?

Concurrency is looking at your app to determine which pieces can run at the same time, and possibly in a random order, yet still result in the correct implementation of your data flow.

Devices now have more than a single CPU. Having more than one core means they are capable of running more than a single task at the same time. By splitting your app into pieces of code you enable the iOS device to run multiple parts of your program at the same time, and this will help to improve the overall performance.

Why use concurrency?

When you’re creating your app, you want it to run as smooth as possible, you don’t want the user to feel that there’s any heavy work that is being done while he is using the app, the user doesn’t care about the API calls or any tasks that are happening and he isn’t seeing, even you as a user hate it when the app freezes as a sudden when you’re using it. what if the app doesn’t freeze at all and you can use the app freely, that’s what concurrency help you do. It can help you separate all the heavy work on the background and make the app responsive for the user without any problem.

How to use concurrency?

Apple provides two main ways to provide you with the ability to run code concurrently using Swift:

  1. Grand Central Dispatch or (GCD)
  2. Operation Queue

Grand Central Dispatch or (GCD)

  • Is a framework to manage concurrent operations.
  • Provided and manages the FIFO Dispatch Queue

There are two types of dispatch queues:

  1. Serial Queue: Tasks are executed one at a time, which means after one task completes then the next task will start.
  • Accessed by DispatchQueue.main
  • Mainly used for updating the UI
  • This is the highest priority queue

2. Concurrent Queue: Tasks are dequeued serially and executed at the same time to run at once and can finish in any order.

  • Accessed by DispatchQueue.global()
  • Have different priorities (High, Default, Low, Background)

Work placed into the queue may either run synchronously or asynchronously.

  • Synchronously execution is done by sync method, the current thread waits until the task finished before the method call returns.
  • Asynchronously execution is done by async method, the method call returns immediately.
// Class level variable
let queue = DispatchQueue(label: "com.shakoushy.gcd")

// Somewhere in your function
queue.async {
// Call slow non-UI methods here

DispatchQueue.main.async {
// Update the UI here
}
}

Quality of service (QoS)

When using a concurrent dispatch queue, you’ll need to tell iOS how important the tasks are, so that it can properly prioritize the work that needs to be done.

let queue = DispatchQueue.global(qos: .background)
  • Highest Priorities:

.userInteractive is recommended for tasks that the user directly interacts with.

.userInitiated is used when the user kicks off a task from the UI that needs to happen immediately but can be done asynchronously.

.utility is used for tasks that would typically include a progress indicator such as long-running computations, I/O, networking or continuous data feeds.

  • Lowest Priority:

.background is used for tasks that the user is not directly aware of.

  • Default Priority:

.default is the default value of the qos argument.

If you create your own concurrent dispatch queue, you can tell the system what the QoS is via its initializer:

let queue = DispatchQueue(label: "com.shakoushy.gcd", 
qos: .userInitiated,
attributes: .concurrent)

DispatchGroup

  • It’s used for grouping and chaining tasks
  • it means that the app wouldn’t go to the next step except after finishing all the tasks in the group.
  • we can group several tasks, perform the job in the queue we want, and get a notification when all the tasks have been completed using these methods .enter(), .leave(), .notify()

Example:

let workingQueue = DispatchQueue(label: "com.shakoushy.gcd", attributes: .concurrent)
let globalQueue = DispatchQueue.global()
let defaultGroup = DispatchGroup() //create a new group

func multiplication(_ num: (Int, Int)) -> Int{
sleep(1) //to make the method slower
return num.0 * num.1
}

let groupOfNumbers = [(1, 1), (5, 2), (3, 4)]

for pair in groupOfNumbers{
//group of task assigning in working queue
workingQueue.async(group: defaultGroup){
let result = multiplication(pair)
print("Result: \(result)")
}
}

//notification
defaultGroup.notify(queue: globalQueue){
print("Multiplication Done!")
}
Result: 12
Result: 10
Result: 1
Multiplication Done!

Output:

Result: 12
Result: 10
Result: 1
Multiplication Done!

As the workingQueue is concurrent, so you see the output of the result is not ordered serially.

DispatchSemaphore

  • Consists of a threads queue and a counter value (type Int).
  • Signaling mechanism used to control access to shared resources.
  • Allow us to lock to a number of threads.
  • Solve multiple problems like the reader-writer lock problem
  • it has two functions .wait(), .signal()

Calling wait() will do the following:

  • Decrement semaphore counter by 1.
  • If the resulting value is less than zero, the thread is frozen.
  • If the resulting value is equal to or bigger than zero, the code will get executed without waiting.

Calling signal() will do the following:

  • Increment semaphore counter by 1.
  • If the previous value was less than zero, this function wakes the oldest thread currently waiting in the thread queue.
  • If the previous value is equal to or bigger than zero, it means the thread queue is empty, aka, no one is waiting.

let’s consider the following real-life scenario:

A father sits with his three kids at home, then he pulls out an iPad…

Kid 2: I want to play with the iPad!!!
Kid 1: NO!, I want to play first…
Kid 3: Ipad! Ipad! Ipad! *sound of claps*
Father: Ok, Kid 2, since you asked first and no one is currently using the iPad, take it, but let me know once you are done. Rest of kids, please wait patiently.
Kid 2: (5 min later) I’m done father.
Father: Kid 1, the iPad is available, let me know once you are done.
Kid 1: (5 min later) I’m done father.
Father: Kid 3, the iPad is available, let me know once you are done.
Kid 3: (5 min later) I’m done father.

let semaphore = DispatchSemaphore(value: 1)
DispatchQueue.global().async {
print("Kid 1 - wait")
semaphore.wait()
print("Kid 1 - wait finished")
sleep(1) // Kid 1 playing with iPad
semaphore.signal()
print("Kid 1 - done with iPad")
}DispatchQueue.global().async {
print("Kid 2 - wait")
semaphore.wait()
print("Kid 2 - wait finished")
sleep(1) // Kid 1 playing with iPad
semaphore.signal()
print("Kid 2 - done with iPad")
}DispatchQueue.global().async {
print("Kid 3 - wait")
semaphore.wait()
print("Kid 3 - wait finished")
sleep(1) // Kid 1 playing with iPad
semaphore.signal()
print("Kid 3 - done with iPad")
}

Console:

OperationQueue

  • The upper layer abstraction of GCD.
  • Is responsible for scheduling and running operations in the background thread.
  • Is used to control dependencies between tasks.
  • Observe using the KVO(Key-Value Observing).
  • It has control operations(Paused, resumed, canceled).
  • it also can control the maximum number like semaphore.

Example:

let pairOfNumbers  = [(2, 3), (5, 10), (4, 5)]
let operationQueue = OperationQueue()

// if set 1, it will be serial if commented it will be concurrent
operationQueue.maxConcurrentOperationCount = 1

for pair in pairOfNumbers{
operationQueue.addOperation {
let result = pair.0 * pair.1
print("Result: \(result)")
sleep(1)
}
}

Output:

Result: 6
Result: 50
Result: 20

GCD vs OperationQueue

  • GCD is faster and lighter than operations.
  • GCD is ideal to dispatch a block without the hassle of creating OperationQueue.
  • OperationQueue is a higher-level abstraction.
  • OperationQueue support dependencies.
  • OperationQueue monitor state.
  • OperationQueue has paused, resumed, canceled.
  • OperationQueue can limit the number of tasks easily.

The Cost of concurrency

  • Deadlock: Two operations are waiting for each other.
  • Priority inversion: Low priority task is dependent on high priority task.
  • Producer-Consumer Problem: Thread creating a data resource while another is accessing it.

In the end

If you’ve reached the end I hope you’ve got what you wanted by reading this article, iOS Concurrency is a very very big topic and has a lot of things you can explore and use to optimize your code and your app’s performance. I’ve tried to cover all what I can in a simple way but you still need to search and look for more, every headline is a huge topic that has more details.

Thank you for reading! If you liked this article, please clap so other people can read it too :)

Connect with me on LinkedIn ;)

--

--