[Interview] Concurrent vs. Serial Dispatch Queues in Swift

Mohit Dubey
7 min readMay 29, 2024

--

The differences between serial and concurrent dispatch queues in Swift, demonstrating their impact on performance and execution order through image download examples

Photo by Fleur on Unsplash

In the world of iOS development, understanding the nuances of concurrent and serial dispatch queues is crucial for optimizing performance and ensuring a smooth user experience. In this article, we will build on concepts discussed in the previous post on async and sync execution flow and thread names. If you haven’t read it yet, I recommend you do so: [Interview] DispatchQueues sync vs async Explained

Introduction to DispatchQueues

DispatchQueues are a fundamental part of Grand Central Dispatch (GCD) in Swift, providing a way to execute tasks asynchronously or synchronously. By default, DispatchQueues are serial, meaning they execute tasks one at a time, in the order they are added. To enable concurrent execution, you need to initialize the queue with a concurrent attribute.

What is a Concurrent Queue?

A concurrent queue allows multiple tasks to run at the same time, though not necessarily in parallel. This means that while tasks can start and progress simultaneously, they may still be executed on different threads, involving thread switching. Concurrent queues are beneficial when you have tasks that can run independently of each other, such as downloading multiple images simultaneously.

When and Why to Use a Concurrent Queue?

In scenarios where tasks are independent and can run simultaneously without waiting for each other, using a concurrent queue can improve performance by utilizing system resources more efficiently. For example, downloading multiple images at the same time or performing multiple calculations concurrently can significantly speed up the process.

Image Download Implementation: Serial vs. Concurrent

Let’s explore the implementation of image downloads using both serial and concurrent dispatch queues.

Image download on serial queue

In the below example, we are downloading the image on imageDownloadQueue DispatchQueue. We are initiating two async operations. One with print statements with image download from 0 to 5 and the other with async statements with image download from 6 to 10. Image download is the time consuming tasks here so it makes sense to do in on the non main thread. However these images are independent of each other so the sequence in which it will download, does not matter to us. The imageDownloadQueue is suppose to some more implementations as well apart from just image download so let’s see what the output will be

extension Thread {
var threadName: String {
if let currentOperationQueue = OperationQueue.current?.name {
return "OperationQueue: \(currentOperationQueue)"
} else if let underlyingDispatchQueue = OperationQueue.current?.underlyingQueue?.label {
return "DispatchQueue: \(underlyingDispatchQueue)"
} else {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8) ?? Thread.current.description
}
}
}

print(String(repeating: "-", count: 20), "Experiment #1a", String(repeating: "-", count: 20))
let imageDownloadQueue = DispatchQueue(label: "ImageDownloadQueue", qos: .utility)

func imageDownload() {
print("<\(Thread.current.threadName)> Before thread start")
imageDownloadQueue.async {
print("imageDownloadQueue.async 0...5 Before Loop")
for i in 0...5 {
do {
print("<\(Thread.current.threadName)> Starting to download image \(i)")
if let url = URL(string: "https://images.pexels.com/photos/1209843/pexels-photo-1209843.jpeg") {
let data = try Data(contentsOf: url)
}
print("<\(Thread.current.threadName)> Downloaded image \(i)")
} catch {
print("Exception in downloading image \(error)")
}
}
}

imageDownloadQueue.async {
print("imageDownloadQueue.async 6...10 Before Loop")
for i in 6...10 {
do {
print("<\(Thread.current.threadName)> Starting to download image \(i)")
if let url = URL(string: "https://images.pexels.com/photos/1209843/pexels-photo-1209843.jpeg") {
let data = try Data(contentsOf: url)
}
print("<\(Thread.current.threadName)> Downloaded image \(i)")
} catch {
print("Exception in downloading image \(error)")
}
}
}
}

imageDownload()

//This will be called on main queue
imageDownloadQueue.sync {
print("<\(Thread.current.threadName)> ImageDownloadQueue sync call")
}

imageDownloadQueue.async {
print("<\(Thread.current.threadName)> ImageDownloadQueue async call")
}
print(String(repeating: "-", count: 20), "<\(Thread.current.threadName)> End of Experiment #1a", String(repeating: "-", count: 20))

— — — — — — — — — — Experiment #1a — — — — — — — — — —
<OperationQueue: NSOperationQueue Main Queue> Before thread start
imageDownloadQueue.async 0…5 Before Loop
<ImageDownloadQueue> Starting to download image 0
<ImageDownloadQueue> Downloaded image 0
<ImageDownloadQueue> Starting to download image 1
<ImageDownloadQueue> Downloaded image 1
<ImageDownloadQueue> Starting to download image 2
<ImageDownloadQueue> Downloaded image 2
<ImageDownloadQueue> Starting to download image 3
<ImageDownloadQueue> Downloaded image 3
<ImageDownloadQueue> Starting to download image 4
<ImageDownloadQueue> Downloaded image 4
<ImageDownloadQueue> Starting to download image 5
<ImageDownloadQueue> Downloaded image 5
imageDownloadQueue.async 6…10 Before Loop
<ImageDownloadQueue> Starting to download image 6
<ImageDownloadQueue> Downloaded image 6
<ImageDownloadQueue> Starting to download image 7
<ImageDownloadQueue> Downloaded image 7
<ImageDownloadQueue> Starting to download image 8
<ImageDownloadQueue> Downloaded image 8
<ImageDownloadQueue> Starting to download image 9
<ImageDownloadQueue> Downloaded image 9
<ImageDownloadQueue> Starting to download image 10
<ImageDownloadQueue> Downloaded image 10
<OperationQueue: NSOperationQueue Main Queue> ImageDownloadQueue sync call
— — — — — — — — — — <OperationQueue: NSOperationQueue Main Queue> End of Experiment #1a — — — — — — — — — —
<ImageDownloadQueue> ImageDownloadQueue async call

Here, the execution is strictly serial. The queue processes images 0–5 first, followed by 6–10, and finally executes the synchronous block. This serialized approach ensures tasks are executed one after another, maintaining the order of operations.

Image download on concurrent queue

By initializing imageDownloadQueue with a concurrent attribute, we can enable concurrent execution. Modify the queue initialization as follows:

let imageDownloadQueue = DispatchQueue(label: “ImageDownloadQueue”, qos: .utility, attributes: [.concurrent])

extension Thread {
var threadName: String {
if let currentOperationQueue = OperationQueue.current?.name {
return "OperationQueue: \(currentOperationQueue)"
} else if let underlyingDispatchQueue = OperationQueue.current?.underlyingQueue?.label {
return "DispatchQueue: \(underlyingDispatchQueue)"
} else {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8) ?? Thread.current.description
}
}
}

print(String(repeating: "-", count: 20), "Experiment #1a", String(repeating: "-", count: 20))
let imageDownloadQueue = DispatchQueue(label: "ImageDownloadQueue", qos: .utility, attributes: [.concurrent])

func imageDownload() {
print("<\(Thread.current.threadName)> Before thread start")
imageDownloadQueue.async {
print("imageDownloadQueue.async 0...5 Before Loop")
for i in 0...5 {
do {
print("<\(Thread.current.threadName)> Starting to download image \(i)")
if let url = URL(string: "https://images.pexels.com/photos/1209843/pexels-photo-1209843.jpeg") {
let data = try Data(contentsOf: url)
}
print("<\(Thread.current.threadName)> Downloaded image \(i)")
} catch {
print("Exception in downloading image \(error)")
}
}
}

imageDownloadQueue.async {
print("imageDownloadQueue.async 6...10 Before Loop")
for i in 6...10 {
do {
print("<\(Thread.current.threadName)> Starting to download image \(i)")
if let url = URL(string: "https://images.pexels.com/photos/1209843/pexels-photo-1209843.jpeg") {
let data = try Data(contentsOf: url)
}
print("<\(Thread.current.threadName)> Downloaded image \(i)")
} catch {
print("Exception in downloading image \(error)")
}
}
}
}

imageDownload()

//This will be called on main queue
imageDownloadQueue.sync {
print("<\(Thread.current.threadName)> ImageDownloadQueue sync call")
}

imageDownloadQueue.async {
print("<\(Thread.current.threadName)> ImageDownloadQueue async call")
}
print(String(repeating: "-", count: 20), "<\(Thread.current.threadName)> End of Experiment #1a", String(repeating: "-", count: 20))

Now, here the threads will start to execute in concurrently and you will notice a lot of thread switch and the output may look something like this:

— — — — — — — — — — Experiment #1a — — — — — — — — — —

<OperationQueue: NSOperationQueue Main Queue> Before thread start
imageDownloadQueue.async 0…5 Before Loop
imageDownloadQueue.async 6…10 Before Loop
<OperationQueue: NSOperationQueue Main Queue> ImageDownloadQueue sync call
<ImageDownloadQueue> Starting to download image 0
<ImageDownloadQueue> Starting to download image 6
<ImageDownloadQueue> ImageDownloadQueue async call
— — — — — — — — — — <OperationQueue: NSOperationQueue Main Queue> End of Experiment #1a — — — — — — — — — —
<ImageDownloadQueue> Downloaded image 6
<ImageDownloadQueue> Starting to download image 7
<ImageDownloadQueue> Downloaded image 0
<ImageDownloadQueue> Starting to download image 1
<ImageDownloadQueue> Downloaded image 7
<ImageDownloadQueue> Starting to download image 8
<ImageDownloadQueue> Downloaded image 1
<ImageDownloadQueue> Starting to download image 2
<ImageDownloadQueue> Downloaded image 8
<ImageDownloadQueue> Starting to download image 9
<ImageDownloadQueue> Downloaded image 9
<ImageDownloadQueue> Starting to download image 10
<ImageDownloadQueue> Downloaded image 2
<ImageDownloadQueue> Starting to download image 3
<ImageDownloadQueue> Downloaded image 10
<ImageDownloadQueue> Downloaded image 3
<ImageDownloadQueue> Starting to download image 4
<ImageDownloadQueue> Downloaded image 4
<ImageDownloadQueue> Starting to download image 5
<ImageDownloadQueue> Downloaded image 5

In this output, you can see that tasks start and finish in a non-sequential manner, demonstrating thread switching and concurrent execution. However, the execution is concurrent but not parallel, meaning tasks share resources and may still wait for each other at times. Parallel execution will depend on available cores at that moment

Key Observations

  • Serial Execution: Tasks are executed one at a time, maintaining a strict order.
  • Concurrent Execution: Tasks start and progress simultaneously, but not necessarily in parallel. This leads to a non-sequential order of completion.
  • Performance: Concurrent operation takes significantly less time when compared to serial operation.

Conclusion

Understanding when to use serial versus concurrent dispatch queues is vital for optimizing your iOS applications. Use serial queues for tasks that must be performed in a specific order, and concurrent queues for independent tasks that can run simultaneously. While concurrent execution can improve performance, it’s essential to recognize its limitations and the distinction from parallel execution.

By leveraging the right type of queue for your tasks, you can enhance the efficiency and responsiveness of your applications, providing a better user experience.

  • For any queries related to the article or mobile system design interviews, please leave a comment.
  • If you have doubts about any of the steps, drop a comment.
  • Bookmark this article for future reference and quick access.

How can you encourage me?

  • Share this article with friends and colleagues preparing for mobile system design interviews.
  • Show your appreciation by clapping as much as you like; it encourages me to create more content.
  • Follow my profile to stay updated on new articles.

--

--

Mohit Dubey

Associate Director @ VerSe Innovation(Dailyhunt and Josh) | Gen AI, Machine Learning and Deep Learning | mobile full stack | iOS Swift, React Native, KMP | GO