Concurrency (GCD & NSOperation Queue) Simplified iOS — Swift

For an app, performance & smooth user experience are the two most important factors. Concurrency is one of the feature that help us to increase the performance of our app. In iOS we can achieve concurrency with two Apple provided APIs i.e GCD & NSOperationQueue.

If you have never implemented GCD in your app before, then it’s a great time to start.If you have experience, I think you will still find this article useful.

Before directly jumping to GCD, let’s discuss something about Concurrency & Threading

Concurrency :

Concurrency means multiple computations are happening at the same time.

Threading :

Thread in a program is an execution path. So Threading is creating additional independent execution paths for our program. Every program starts with at least one execution path/thread. You can create more threads to execute parallel tasks depending on your requirements.

Why do we need Concurrency ?

GCD :

GCD is an API that help developers to write multi — threaded code without creating & handling the threads by themselves. With GCD we only have to schedule work tasks(or dispatch task to DispatchQueues), and the system will perform these tasks for us by making the best use of its resources.

So what is a task ?

Task : Tasks can refer to any block of code that we submit to a queue using the sync or async functions. They can be submitted in the form of an anonymous closure or inside a dispatch work item that gets performed later.

Anonymous closure : which doesn’t have a name & can capture value from its surrounding context.

DispatchQueue.global().async {
print("It's a anonymous closure")
}

Dispatch work item :

let item = DispatchWorkItem(qos: .default) {
print("will be executed later")
}

DispatchQueue : These are similar to Queue Data Structure & are the main building block of GCD, to which we submit tasks in the form of Anonymous closure. Execution of those tasks always starts in FIFO(First In First Out) order inside dispatch Queue. ⭐️ Since the compilation of a task depends upon various factors, It’s not guaranteed that the execution will finish in FIFO Order.

Total Threads with in an iOS Application : System provides 2 types of threads to an iOS app :

Total queues available within an iOS Application : GCD provides us 3 types of queues.

 DispatchQueue.main.sync {
print("It's how you can access Main Queue")
}

⭐ ️It’s compulsory that all UI work most be done in Main Queue.

You can turn on the Main Thread Checker option in Xcode to receive warnings whenever UI work gets executed on a background thread.

Select Edit Scheme from Product Menu > Run > Main Thread Checker

2. Global Queues : These are concurrent & are also pre-defined & have varying level of QoS (Quality of Service ).

DispatchQueue.global(qos: .background).async {
print("global queue with background QoS")
}

3. Private Queues : These are created by developers. By default they are serial.

let privateQueue = DispatchQueue(label: "com.debashish.private")
privateQueue.async {
print("Private serial queue")
}

It’s recommended that you always use a reverse domain name (eg : "com.debashish.private") label, while creating privateQueues. It will help you to identify the queue easily during debugging.

You can make a privateQueue concurrent, by providing an optional Parameter i.e attributes

let privateQueue = DispatchQueue(label: "com.debashish.private", attributes: .concurrent)
privateQueue.async {
print("Private concurrent queue")
}

There’s also an optional QoS parameter, where you can assign a QoS to privateQueues.

let backgroundQueue = DispatchQueue(label: "com.debashish.private", qos: .background, attributes: .concurrent)
backgroundQueue.async {
print("private concurrent Queue")
}

For Global & Private queue, GCD will be responsible for selecting any one of the threads from the thread pool that it manages. But except for the main queue, it always runs in Main Thread.

I will discuss Serial & Concurrent, Sync & Async in upcoming parts, I promise

Let’s discuss what’s QoS

QoS (Quality of Service)

A quality of service (QoS) class allows you to categorize task to be performed by GCD, NSOperation, By assigning a QoS to a task, you indicate its importance, and the system prioritizes it and schedules it accordingly.

Types :

Work that is interacting with the user, such as refreshing the user interface, or performing animations. If the work doesn’t happen quickly, the user interface may appear frozen. Focuses on responsiveness and performance.

2. User-Initiated :

Work that the user has initiated and requires immediate results, such as opening a saved document or performing an action when the user clicks something in the user interface.

3. Utility :

Work that may take some time to complete and doesn’t require an immediate result, such as downloading or importing data. Utility tasks typically have a progress bar that is visible to the user.

4. Background :

Work that operates in the background and isn’t visible to the user, such as indexing, synchronizing, and backups.

5. Two special QoS :

a. Default : The priority level of this QoS falls between user-initiated and utility. Work that has no QoS information assigned is treated as default, and the GCD global queue runs at this level.

b. Unspecified : This represents the absence of QoS information and cues the system that an environmental QoS should be inferred

Regardless of whether you dispatch synchronously or asynchronously, and whether you choose a serial or concurrent queue, all of the code inside a single task will execute line by line.

⭐️ Concurrency is only relevant when evaluating multiple tasks.

let privateConcurrentQueue = DispatchQueue(label: "com.debashish.privateQueue", qos: .default, attributes: .concurrent)
privateConcurrentQueue.async {
for _ in 1...5 {
print("🌍")
}
for _ in 1...5 {
print("⭐️")
}
}

It will print five 🌍 & then five ⭐️ sequentially.

By now you might be wondering what serial and concurrent are all about. You might also be wondering about the differences between sync and async when submitting your tasks. So let’s dive in!

Sync vs Async

Both effects the source of the submitted task, Source means the Queue they are submitted from.

⭐ ️what is source or current queue ?

if you call your sync statement inside viewDidLoad, your current queue will be the Main dispatch queue. If you call the same function inside a URLSession completion handler, your current queue will be a background queue.

Sync :

When your code reaches a sync statement, it will block the current queue until that task completes. Once the task returns/completes, control is returned to the current queue. And the code after the sync is executed.

DispatchQueue.global(qos: .default).sync {
print("In global Queue")
}
print("In Main Queue")
OUTPUT :
----------------
In global Queue
In Main Queue

Here, when execution encounters a sync statement, It will block the current Queue (Here I have taken current queue as Main Queue), executes the block so first prints ‘In global Queue’, then again current queue is unblocked, then it prints ‘In main Queue’.

Dispatcher

So what happens when we execute a sync on Main Queue ?

DispatchQueue.main.sync { 
print("main - sync")
}
print("In main Queue")
OUTPUT :
----------
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).

Here, what happened was sync blocked the current queue (i.e Main Queue in this case) and proceed to execute the sync block, but here the block is going to execute in Main Queue, because we have called sync in Main Queue, but Main Queue is already blocked by sync, so it created a deadlock situation.Thats why we got a run time error !!

Async

An async statement, will execute asynchronously with respect to the current queue, and immediately returns control back to the current queue without waiting for the contents of the async closure to execute.

DispatchQueue.global(qos: .default).async {
print("In global Queue")
}
print("In Main Queue")
OUTPUT : (Here output completely depends upon the qos)
----------------
In Main Queue
In global Queue

Our code now submits the closure to the global queue, then immediately proceeds to run the next line. It will likely print “In Main Queue” before “In global Queue”, but this order isn’t guaranteed. It depends on the QoS of the source and destination queues, as well as other factors that the system controls.

Dispatcher

Which one we should use ?

Serial vs Concurrent

Serial and concurrent affect the destination — the queue to which your work (Tasks) has been submitted to run. This is in contrast to sync and async, which affected the source.

Serial

A serial queue will not execute its work on more than one thread at a time, regardless of how many tasks you dispatch on that queue. Consequently, the tasks are guaranteed to not only start, but also terminate, in FIFO order.

let privateQueue = DispatchQueue(label: "com.debashish.private")
privateQueue.async {
for _ in 1...5 {
print("first - task")
}
}
privateQueue.async {
for _ in 1...5 {
print("second - task")
}
}
OUTPUT :
----------------
first - task
first - task
first - task
first - task
first - task
second - task
second - task
second - task
second - task
second - task

privateQueue is a serial Queue, so it will run only in one thread. I have dispatched two tasks to this queue, first one will print ‘first-task’ five times, second task will print ‘second-task’ for five times.It is serial, that means only after the completion of the first task , second task will start.

Dispatcher

Concurrent

A concurrent queue can utilize multiple threads, and the system decides how many threads are created.

Tasks always start in FIFO order, but the queue does not wait for a task to finish before starting the next task, therefore tasks on concurrent queues can finish in any order.

let concurrentQueue = DispatchQueue.global()concurrentQueue.async {
for _ in 1...3 {
print("first")
}
}
concurrentQueue.async {
for _ in 1...3 {
print("second")
}
}
concurrentQueue.async {
for _ in 1...3 {
print("third")
}
}
OUTPUT :
----------
FIRST RUN :
first
third
second
second
second
first
first
third
third
SECOND RUN :
second
third
first
first
first
second
second
third
third

We have provided 3 tasks to concurrent queue, here first task is always first to task, but which one will execute after then , and the termination of tasks is completely depends upon the GCD

Dispatcher

Let’s consider a situation with concurrent queue ..

let concurrentQueue = DispatchQueue.global()
concurrentQueue.sync {
for _ in 1...3 {
print("⭐️")
}
}
concurrentQueue.async {
for _ in 1...3 {
print("🌍")
}
}
OUTPUT :
-----------
⭐️
⭐️
⭐️
🌍
🌍
🌍

Suddenly, our results are back in perfect order. But this is a concurrent queue, so how could that happen? Did the sync statement somehow turn it into a serial queue? No …

What happened is that we did not reach the async call until the first task had completed its execution. The queue is still very much concurrent, but inside this zoomed-in section of the code. it appears as if it were serial. This is because we are blocking the caller, and not proceeding to the next task, until the first one is finished.

⭐️ How to get current queue name ?

with isMainThread you can check if the current thread is the main thread or not

print(Thread.isMainThread)OUTPUT : 
-----------
true (in case of main thread)

Similarly, to get the name of the current queue, you can add a property to DispatchQueue like this :

extension DispatchQueue {
static var currentLabel: String? {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8)
}
}
USE :
----------
print(DispatchQueue.currentLabel ?? "Error") // in main queue
OUTPUT :
--------
"com.apple.main-thread"

Let’s discuss one important concept of GCD :

Dispatch Group :

With dispatch groups we can group together multiple tasks and either wait for them to be completed or be notified once they are complete. Tasks can be asynchronous or synchronous and can even run on different queues. Dispatch groups are managed by DispatchGroup object.

Remember that we can get notified even if tasks are running in Different Threads

4. There are other methods in DispatchGroup, wait() & wait(timeout: )

5. Calling wait() blocks the current thread until the execution of group’s tasks are not completed.

6. Calling wait(timeout: ) blocks the current thread, but after the timeout specified, continues anyway.

for 2 seconds of timeout : wait(timeout: .now() + 2 )
OUTPUT : 
-------
first 1
first 2
first 3
first 4
first 5
second 1
second 2
second 3
second 4
second 5
all complete

Another Example :

I have created below function to download an image asynchronously from web.

Let’s use DispatchGroup to notify when all downloads are complete :

Try to implement group.wait() & group.wait(timeout: ) in above code.

Conclusion

That is all about GCD. Hope you like it. Operation queue will be there in the next part of this article.

Happy Coding ….

iOS Developer || life long learner