(Swift) Grand Central Dispatch (GCD)
Grand Central Dispatch (GCD) is a low-level API for managing operations either asynchronously or synchronously. GCD can be used to manage heavy tasks to the background so that we can improve our app’s responsiveness. In this article, I will summarize the important concepts related to GCD.
Dispatch Queue
GCD is built on top of multiple threads. It manages a shared thread pool under the hood. In GCD we add tasks to dispatch queues so that GCD can decide which thread to execute them on. Dispatch queues are thread-safe which means they can be accessed from different threads simultaneously.
According to Apple’s documentation, a dispatch queue is an object-like structure that manages the tasks you submit to it. All dispatch queues adopt FIFO data structure (first-in, first-out) such as the queue. There are 3 main types of dispatch queues.
1. Main Dispatch Queue
The main dispatch queue is a serial queue which is globally available. It executes tasks on the main thread.
Since anything that modifies the UI must run on the main thread, it is common to use this to update the UI after completing work in a task on a concurrent queue.
2. Concurrent Queues (Global Queues)
In iOS, a task consists of one or multiple threads. Concurrent queues (also known as a type of Global DIspatch Queue which is prepared by the system) execute one or more tasks concurrently. As the name implies, however, it still keeps its FIFO order of execution. The currently executing tasks run on different threads which are managed by the dispatch queue.
There are four such queues which have different priorities: high, default, low, and background. The background priority queue has the lowest priority. When we send tasks to the global concurrent queues, we specify a Quality of Service (QoS) class property which determines the priorities.
userInteractive
:
We use this for UI updates, event handling and small workloads that require low latency. This should run on the main thread.userInitiated
:
The user initiates these asynchronous tasks from the UI. We can use this when the user is waiting for results and for tasks which are required to continue user interaction. This run in the high priority global queue.utility
:
This represents long-running tasks such as a progress indicator which is visible during computations, networking, continuous data fetching, etc. This run in the low priority global queue.background
:
This represents tasks that users are not aware of such as prefetching, maintenance, and other tasks that don’t require user interaction and aren’t time-sensitive. As I mentioned, this has the lowest priority.
DispatchQueue.global(qos: .background).async {
self.doSomething()
}
3. Custom Queues
We can create queues by ourselves. Custom queues can be serial or concurrent. Requests in these queues actually end up in one of the global queues. The code below is the example of processing something in a different queue and taking it back to the main queue.
DispatchQueue(label: "com.kentakodashima.app.sample").async {
// You can do something such as transforming an image.
self.doSomething() DispatchQueue.main.async {
// You can do something such as setting the transformed image to ImageView.
self.doSomething()
}}
Synchronous vs. Asynchronous
Synchronous
A synchronous function proceeds on to the next function after the task completes. We schedule a block of work synchronously by calling DispatchQueue.sync(execute:)
.
Asynchronous
An asynchronous function orders the task to start and returns immediately. However, it does not wait for the task to complete. Therefore, it does not block execution in the current thread from moving on to the next function. We schedule a block of work asynchronously by calling DispatchQueue.async(execute:)
.
Readers-Writers Problem
Readers-writers problem is a common computing problem in concurrency.
In Swift, the constants which are declared with the let
keyword are read-only no matter which thread they are in. Therefore, it is thread-safe.
However, the variables which are declared with the var
keyword is mutable. Thus, it is not thread-safe since modifying the value of a variable in other threads. That might directly affect UI behaviours.
Dispatch Barriers
GCD provides a solution to the problem. Dispatch barriers are a group of functions which act as a serial-style blockage when they work with concurrent queues.
When we submit a DispatchWorkItem
to a dispatch queue, we can set flags to make sure that it should be the only item executed on the specified queue for that particular time. This means all works prior to the dispatch barrier are guaranteed to complete before the DispatchWorkItem
will execute.
let queue = DispatchQueue(
label: "com.kentakodashima.app.sampleBarrier",
attributes: .concurrent
)queue.async(flags: .barrier) {
print("I am the only item to be executed.")
}
Dispatch Groups
DispatchGroup
manages dispatch groups. Using dispatch groups, we can create groups of multiple tasks and either wait for them to complete or receive a notification once they complete. The code below is an example of receiving a notification when all queues in the group are completed.
// Create a group
let dispatchGroup = DispatchGroup()
let queue1 = DispatchQueue(label: "com.kentakodashima.app.sampleQueue1")
let queue2 = DispatchQueue(label: "com.kentakodashima.app.sampleQueue2")
let queue3 = DispatchQueue(label: "com.kentakodashima.app.sampleQueue3")// Put all queues into dispatchGroup
queue1.async(group: dispatchGroup) {
print("Queue1 complete.")
}queue2.async(group: dispatchGroup) {
print("Qqueue2 complete.")
}queue3.async(group: dispatchGroup) {
print("Queue3 complete.")
}// After the queues in dispatchGroup are all done, back to the main thread
dispatchGroup.notify(queue: DispatchQueue.main) {
print("All tasks are done.")
}
In the other way, we can specify when to enter into the group tasks and when to leave. In the example below, print(result)
is executed after all queues which are in between dispatchGroup.enter()
and dispatchGroup.leave()
are completed.
DispatchQueue.global(qos: .default).async {
var result = 0
let array = [1, 4, 5, 8]
let dispatchGroup = DispatchGroup()dispatchGroup.enter()
for element in array {
result += element
}
result /= 4
dispatchGroup.leave()
dispatchGroup.wait()
DispatchQueue.main.async {
print(result)
}
}
That’s everything for this article. If you want to learn deeper, I highly recommend that you go to check the references below. Especially, ‘Grand Central Dispatch Tutorial’ from raywenderlich.com offers a good example of how we can use GCD in the real app.
References:
Concurrent Programming With GCD in Swift 3:
https://developer.apple.com/videos/play/wwdc2016/720/
Dispatch — Apple Developer:
https://developer.apple.com/documentation/dispatch
Dispatch Queues — Apple Developer Archive:
https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html
Grand Central Dispatch Tutorial for Swift 4: Part 1/2 — raywenderlich.com:
https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2
Grand Central Dispatch Tutorial for Swift 4: Part 2/2 — raywenderlich.com:
https://www.raywenderlich.com/5371-grand-central-dispatch-tutorial-for-swift-4-part-2-2