• Jennifer Eve Vega

Swift DispatchQueues

Updated: Jun 23



Perhaps there are other iOS Engineers with years of experience but still feel overwhelmed when we talk about concurrency or threading topics. Just like me. I've been working as an iOS Engineer and I didn't try to learn this at all, I felt like I wasn't ready until few weeks ago.


Serial vs. Concurrent

Serial simply means it is running in a single thread (it can be in the main thread, or a background thread), and it can only run one task at any given time.

Concurrent means it is running in multiple threads (depending on how many resources the system can allocate for you)


DispatchQueue is by default a serial queue. If you implement it this way:

DispatchQueue.main.async {
	// This is a serial queue on the main thread
}
DispatchQueue.global().async {
	// Another serial queue on a different thread
}

How to make it concurrent?

let queue = DispatchQueue(
        label: "Download API BG Thread",
        qos: .background,
        attributes: .concurrent)

Adding multiple tasks in this queue means that it can be run concurrently.

queue.async {
	// Anything inside this block is a `task`
}
queue.async {
	// Example: API Service Call
}

I say it "can" be run, because we are not sure if the system has multiple available threads that we can use. Again, we have no way of knowing, but at least, if it has multiple threads available, then we can be sure that this will be run concurrently.


What is qos?

It means Quality of Service. The tasks are prioritized according to the qos defined.


.userInteractive

- Any UI-updating tasks. Any tasks with this type of qos is prioritized to make sure that the UI is responsive and fast.

.userInitiated

- Any task initiated by the user that can be done asynchronously, but since the user is waiting for that task to finish, this qos has high prioritynext to .userInteractive.

.utility

- For any tasks that have progress indicator because it takes a long time to complete, also some continuous feeds, or perhaps stream API calls.

.background

- Any task that the user is not aware of.

.default

- Should not be used explicitly, and this is the default value for qos argument.

.unspecified

- Should not be used explicitly as well, and is here to support legacy APIs that may opt out of qos


Switching Queues

Perhaps you already learned that it's possible to switch queues. For example, after downloading an image, you have to update the UIImageView with the said image.


DispatchQueue.global().async { [weak self] in
	guard let self = self else { return }
	// Download code

	// Switch to main queue to update the UI
	DispatchQueue.main.async {
		// Update imageView
	}
}

DispatchWorkItem

Aside from putting our task inside the DispatchQueue block (like the previous examples above), there's another way of adding a task to a queue.


For example, instead of adding your tasks inside like this:

let queue = DispatchQueue(label: "Sample Queue")
queue.async {
	print("My Task")
}

You can do it this way, too:

let queue = DispatchQueue(label: "Sample Queue")
let workItem = DispatchWorkItem {
	print("My Task")
}
queue.async(execute: workItem)

One of the advantages of using DispatchWorkItem is that we can cancel the workItem.

	workItem.cancel()

If it hasn't started yet, it will be removed from the queue, if it has already started, then the isCancelled will be set to true.


And we can also call notify(queue:execute:) if we want to perform a specific task after the workItem is done. For example:

let queue = DispatchQueue(label: "Sample Queue")
let downloadWorkItem = DispatchWorkItem {
	// Download task
}
let updateUIWorkItem = DispatchWorkItem {
	// Update UI
}
downloadWorkItem.notify(queue: DispatchQueue.main, 
					   execute: updateUIWorkItem)
queue.async(execute: downloadWorkItem)

This means that after the downloadWorkItem is finished, it will call the updateUIWorkItem on the main queue.


DispatchGroup

There are times that we need to execute a group of jobs, and they may not run at the same time, you need to know when they will finish.

let queue = DispatchQueue(label: "Sample Queue")
let group = DispatchGroup()
let workItem1 = DispatchWorkItem {
	// Task1
}
let workItem2 = DispatchWorkItem {
	// Task2
}
queue.async(group: group, execute: workItem1)
queue.async(group: group, execute: workItem2)

group.notify(queue: DispatchQueue.main) {
	debugPrint("All tasks are finished")
}

This whole Dispatch thing now sounds really simple! So I went ahead and tried to implement it in some parts of our project, I was fixing a bug and noticed that I can apply this new knowledge.


This is the scenario, I have a list of items, and I need to call an API for each of these items. Imagine calling a Detail API given the item's ID.

let detailService = DetailService()

for item in items {
	detailService.getDetail(of: item.id) 
	.subscribe(onNext: { [weak self] detail in
		self?.detailList.append(detail)
	})
	.dispose(by: bag)
}

I converted the code above to call the API concurrently.

private let apiDetailQueue = DispatchQueue(
        label: "detail queue",
        qos: .utility,
        attributes: .concurrent)
// Inside a function
let group = DispatchGroup()
for item in items {
	let workItem = DipatchWorkItem {
		group.enter()
		detailService.getDetail(of: item.id) 
		.subscribe(onNext: { [weak self] detail in
			self?.detailList.append(detail)
			group.leave()
		})
		.dispose(by: bag)
	}
	apiDetailQueue.async(group: group, execute: workItem)
}

group.notify(queue: DispatchQueue.main) {
     debugPrint("DONE: \(self.detailList.count)")
}

But there's a problem after running this code. I can confirm that it really called the API, it also returned the detail, and after it has called the API for all items, it notified that the group tasks have completed and printed:

DONE: 0

Why was the detailList count empty? I have definitely added the result to the detailList.


Thread Safe Variable

So I realized that every time the self?.detailList.append(detail) was executed, it was on a different thread, and take note, each of these tasks may be executed in different threads, so I am trying to update my variable detailList from different threads. Each thread will read and write to the same shared resource, the detailList. This is what they call Race Conditions. So I needed to make my detailList variable thread safe.


One way to solve Race Conditions is to do it in a serial queue. But, I wanted to do this concurrently, so a serial queue was not the solution I was looking for (although, to be honest, I did try and converted all these back to a serial queue).


If we have a variable that will be accessed concurrently, how do we make a thread safe variable?

- We should have a private queue for this variable

- Use a thread barrier


Thread Barrier

It means that we are going to lock down the queue and wait for the update to complete before continuing to the next update. Which means that every update task to our variable will have to wait for their turn to be executed. In this way, we can be sure that we have the latest value before updating to the next value.

private let threadSafeUpdateResultsQueue = DispatchQueue(
        label: "Thread Safe Queue",
        attributes: .concurrent)
private var _detailList = [ItemDetail]()
public var detailList: [ItemDetail] {
        get {
            return threadSafeUpdateResultsQueue.sync {
                return _detailList
            }
        }
        set {
            threadSafeUpdateResultsQueue.async(flags: .barrier) { [unowned self] in
                self._detailList = newValue
            }
        }
    }


After updating my variable to be thread safe, and ran the app again, I am now able to see the count > 0.

DONE: 10

I already made a mental list of what we can improve in our app, and perhaps you can relate on these items, too.

- App Launch

Do you have services that you need to initialize the moment the app launches? Perhaps it can be done concurrently.

- Local DB

Most database implementations are not thread safe. How about creating private queues for write transaction per entity? And create a different, concurrent queue per entity for reading?

- Check for app lags

Perhaps you're blocking the main thread, how about moving the task to a different queue and switch back to main thread if you need to update the UI.

- Looping API calls

You can now implement it concurrently like what I did above

 

I'm still quite new to concurrency, if you have suggestions, or comments about my post, if you think I did something I shouldn't have, let me know and I can test it out.


55 views0 comments