Mastering Grand Central Dispatch (GCD) for iOS Interviews: Practical Examples and Real-World Scenarios. Part — 2

Sajal Gupta
4 min readJun 12, 2023

--

Hello again, my friends! Welcome to the second post in our series on “Mastering Grand Central Dispatch.” In case you missed the first post, you can find it by clicking here.

These articles are presented in chronological order, so it is recommended to read the previous one before diving into this post. This will help you better understand the concepts and build a solid foundation before moving forward.

Grand Central Dispatch (GCD) is a powerful framework provided by Apple that simplifies multithreading and task scheduling. In this article, we will explore key concepts of GCD and dive into practical examples to help you master multithreading in iOS development.

Quality of Service:

GCD provides different quality of services (QoS) levels to prioritize tasks based on their importance and urgency. We have five QoS levels:

  • Default: The default QoS level for tasks that don’t require special priority.
  • UserInteractive: For tasks that demand an instant response to provide a smooth user experience.
  • UserInitiated: Suitable for tasks that are nearly instantaneous, such as fetching data for immediate display. eg any database operation (fetching, deleting, or inserting)
  • Background: Designed for long-running tasks, such as downloading large files or performing complex computations in the background.
  • Utility: The Utility quality of service is suitable for tasks that may take a significant amount of time to complete, such as making API calls or performing lengthy computations.

Understanding Sync and Async:

GCD offers two main types of task scheduling: synchronous (sync) and asynchronous (async).

  • Sync: A sync operation blocks the current thread until the task is completed. It’s important to use sync carefully to avoid deadlocks.
  • Async: An async operation allows the current thread to continue executing without waiting for the task to finish. It’s ideal for non-blocking operations.

Serial and Concurrent Queues:

GCD provides two types of queues: serial and concurrent.

  • Serial Queue: Tasks in a serial queue are executed in the order they are added, one at a time. It ensures thread safety but may lead to slower execution.
  • Concurrent Queue: Tasks in a concurrent queue can be executed concurrently on multiple threads, enhancing performance. However, careful synchronization may be required to handle shared resources.

Code Example 1:

We will explore practical code examples to illustrate the concepts discussed above. These examples will cover scenarios such as executing tasks on the main queue, creating custom queues, managing dependencies between tasks, and utilizing different QoS levels.

let concurrent = DispatchQueue(label: "com.concurrent", attributes: .concurrent)

print("entry")

concurrent.async {
concurrent.async {
print("2")
}
print("1")
}

print("exit")

The output of this code snippet is non-deterministic and can vary between different runs. The possible outputs are:

Output 1:

entry
exit
1
2

Output 2:

entry
exit
2
1

Output 3:

entry
2
exit
1

It can be any combination of print("1"), print("2"), or print("exit").

Hereafter the print("entry"), the main thread and the concurrent queue thread are simultaneously attempting to execute tasks. Consequently, there is a lack of synchronization between these threads both run independently.

The order of the print statements inside and outside the concurrent.async the block is not guaranteed due to the nature of concurrent queues. Since concurrent queues can create and manage multiple threads that run independently and in parallel with the current thread in which we are dispatching the task (in this case, the main thread), there is no inherent synchronization between these tasks.

The specific order of execution depends on how the tasks are scheduled and executed on the concurrent queue, which can vary between different runs or system conditions.

However, the print("entry") statements will always execute before and after the concurrent.async block, respectively.

It’s important to note that the concurrent nature of the queue allows for interleaved execution of tasks, and the exact order cannot be predetermined. Developers should not rely on a specific order of execution when using concurrent queues.

By utilizing GCD’s concurrent queues, developers can take advantage of parallel processing and improve the performance of their applications.

Code Example 2:

Let’s consider another code snippet:

let serial = DispatchQueue(label: "com.serial")

serial.async {
print("entering")

serial.sync {
print("inner block")
}

print("outer block")
}

In this example, we have a serial queue where the tasks are executed in a sequential manner. When we dispatch the task asynchronously using, serial.asyncthe print statement "entering" is executed. However, a deadlock occurs when we try to execute self.serial.sync within the serial queue. This is because the sync operation blocks the current thread until the task is completed, but we are already within the serial queue, causing a circular dependency and resulting in a deadlock.

For a more detailed explanation of deadlocks, I encourage you to refer to my previous article.

Code Example 3:

In the given code snippet, we have an action method called actionOne which is invoked in response to a user interaction. Let's analyze its behavior:

@IBAction func actionOne(_ sender: Any) {
DispatchQueue.main.async { [unowned self] in
print("async started")
self.timeIntensiveTask()
print("async ended")
}
print("sync task started")
timeIntensiveTask()
print("sync task ended")
}

private func timeIntensiveTask() {
var counter = 0
for _ in 0..<1000000000 {
counter += 1
}
}

The expected output of this code will always be:

sync task started
sync task ended
async task started
async task ended

The async task will only start after the sync task because DispatchQueue.main tasks are scheduled to run at the end of the Main Thread’s RunLoop. However, in this case, the sync task is blocking the RunLoop, causing the async task to be executed after the sync task completes. If actionOne were executed on a different thread or the async task was dispatched on a different DispatchQueue, the tasks could potentially start together, with the order depending on the speed of the async task's dispatch.

You can find more information about DispatchQueues and their behavior in the article Understanding DispatchQueues.

Thank you for reading! Please 👏 clap, leave a comment, and follow I really appreciate it! It helps me to know what content people are interested in.

--

--