Concurrency using GDC in Swift — Part 1

task handover explanation

Grand Central Dispatch (GCD) also known as Dispatch is a low-level API provided by Apple in order to manage the concurrent operations of multicore processors in macOS, iOS, watchOS, and tvOS.

Concurrency — Multiple threads are executed at the same time parallelly (depending on the availability of the system threads).

GCD manages the shared thread pool underneath in order to manage tasks.

Queues -
1. GCD operates on Dispatch queues — a class called ‘DispatchQueue’.
2. Queues can be Serial or Concurrent. Serial queue guarantees that only one task is executed at a given time. A concurrent queue allows multiple tasks to be executed at any time.
3. And the tasks on these queues will be executed in FIFO (First In First Out) order. i.e that task that is added first will be executed first but is not guaranteed to finish first.

There are 3 main queues provided by GCD:
1. main queue — it runs on the main thread
2. Global queue — they are shared by the whole system. There are 4 priorities using this queue (default, high, low, background). You can provide priorities using QoS classes.
3. Custom queue — you can create your own custom queues. Your custom queue in turn executing one of the global queues.

QoS classes:
1. User Interactive — The task that must return immediately in order to provide a good user experience. The task should run on the main thread. The amount of work you do must be small.
2. User-Initiated — Use this for asynchronous tasks when a user is waiting for an update in UI. They execute in high priority global queue.
3. Utility — Use this when there are long-running tasks, and you can use progress bars to indicate the progress of the running task. They execute in low priority in the global queue.
4. Background — Use this when you need to perform clean-ups, maintenance tasks, where user interaction is not needed. They execute in the background priority in the global queue.

Synchronous v/s Asynchronous
GCD can perform tasks synchronously or asynchronously.
Synchronous — when control returns to caller after execution of task have been completed. DispatchQueue.sync(execute:).
*'sync' makes the task to run on a given queue. (therefore it is not advisable to run synchronous task on a main queue as this may cause deadlocks.)

Asynchronous — when control returns to the caller immediately, just after executing a task and ready to execute the next one.
*Task will run on a different thread when 'async'.
DispatchQueue.async(execute:)

DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
let overlayImage = self.hueImageFrom(self.image)
DispatchQueue.main.async { [weak self] in
self?.imageView.image = overlayImage
}
}

DispatchQueue.global(qos: .userInitiated).async { this is an async operation for running heavy tasks on a different queue, in order not to block the current queue (here, the main queue — the UI)

DispatchQueue.main.async { you create another async from concurrent queue to return the control to the main queue. This is needed for updating UI on the main queue.
They are called one after the other because once you execute a task concurrently you require to shift back to the main queue in order to update the UI.

Serial v/s Concurrent
All tasks added to the queue are in order.

Serial — in serial queues tasks are executed in order they are added.
tasks are executed one at a time. (Next task has to wait for the 1st task to finish)

Concurrent — here multiple threads are attached to the concurrent queue if system comprises of multiprocessor (most systems now a days). Or time slicing process on single core processor.

execute test1
<NSThread: 0x600001ce4480>{number = 7, name = (null)}
Task 1
execute test2
<NSThread: 0x600001ce4480>{number = 7, name = (null)}
Task 2
execute test3
<NSThread: 0x600001ce4480>{number = 7, name = (null)}
Task 3
** irrespective of how much time each tasks take to execute. And each task run asynchronously themselves but the result will be in an order.
** serial queue — therefore next task will start once the 1st task has been finished executing. As single thread is used to run tasks.
** async — task will be run on a different queue.

let customSerialAsyncExampleQueue = DispatchQueue(label: “test.GCD.SerialAsyncExample”) // serial queue

/*
Requirements:
1. To execute the tasks in order
2. without blocking the current queue
*/

// 3 tasks

func test1() {
print(“execute test1”)
print(Thread.current)
Thread.sleep(forTimeInterval: 1.0)
print(“Task 1”)
}

func test2() {
print(“execute test2”)
print(Thread.current)
Thread.sleep(forTimeInterval: 10.0)
print(“Task 2”)
}

func test3() {
print(“execute test3”)
print(Thread.current)
Thread.sleep(forTimeInterval: 2.0)
print(“Task 3”)
}

public func serialAsyncExample() {
customSerialAsyncExampleQueue.async {
test1()
test2()
test3()
}
}

Example 2, serial queue and running tasks synchronous
difference between 2 examples here,
** this example runs on synchronously therefore it will run on a current queue. (in this case a main queue)
output:
execute test4
<NSThread: 0x600000a74940>{number = 1, name = main}
Task 4
execute test5
<NSThread: 0x600000a74940>{number = 1, name = main}
Task 5
execute test6
<NSThread: 0x600000a74940>{number = 1, name = main}
Task 6

let customQueueSerialSyncExampleQueue = DispatchQueue(label: “test.GCD.serialSyncExample”) // serial queue

// 3 tasks

func test4() {
print(“execute test4”)
print(Thread.current)
Thread.sleep(forTimeInterval: 3.0)
print(“Task 4”)
}

func test5() {
print(“execute test5”)
print(Thread.current)
Thread.sleep(forTimeInterval: 2.0)
print(“Task 5”)
}

func test6() {
print(“execute test6”)
print(Thread.current)
Thread.sleep(forTimeInterval: 5.0)
print(“Task 6”)
}

public func serialSyncExample() {
customQueueSerialSyncExampleQueue.sync {
test4()
test5()
test6()
}
}

** since async — tasks will be run on a different threads
** concurrent — multiple threads are used in executing tasks

output:

1 task started
2 task started
3 task started
Other thread <NSThread: 0x600001a19500>{number = 4, name = (null)}
Other thread <NSThread: 0x600001a094c0>{number = 7, name = (null)}
Other thread <NSThread: 0x600001a14cc0>{number = 5, name = (null)}
3 task finished
1 task finished
2 task finished

let customQueueConcurrentASyncExampleQueue = DispatchQueue(label: “test.GCD.ConcurrentASyncExample”, attributes: .concurrent) // concurrent queue

public func concurrentASyncExample() {
for i in 1…3 {
print(“\(i) task started”)
customQueueConcurrentASyncExampleQueue.async {
if Thread.isMainThread {
print(“Main thread \(Thread.current)”)
} else {
print(“Other thread \(Thread.current)”)
}
print(“\(i) task finished”)
}
}
}

Example 4, concurrent queue and running tasks synchronously
** since sync — tasks will be run on the given thread (in this case, the main thread)
** concurrent — multiple threads are used in executing tasks. Although we have provided these tasks to run as concurrent, this doesn't play much role when we process sync tasks on the main thread as main thread has a serialised queue and process running on main thread has the highest priority. Here concurrency does not play any role.
output:

1 task started
Main thread <NSThread: 0x600000a0c200>{number = 1, name = main}
1 task finished
2 task started
Main thread <NSThread: 0x600000a0c200>{number = 1, name = main}
2 task finished
3 task started
Main thread <NSThread: 0x600000a0c200>{number = 1, name = main}
3 task finished

let customQueueConcurrentSyncExampleQueue = DispatchQueue(label: “test.GCD.ConcurrentSyncExample”, attributes: .concurrent) // concurrent queue

public func concurrentSyncExample() {
for i in 1…3 {
print(“\(i) task started”)
customQueueConcurrentSyncExampleQueue.sync {
if Thread.isMainThread {
print(“Main thread \(Thread.current)”)
} else {
print(“Other thread \(Thread.current)”)
}
print(“\(i) task finished”)
}
}
}

Dispatch groups can group together multiple tasks and can either wait for them to finish or get notified when they have finished.
You should be very careful not to call DispatchGroup on the main thread, because the main thread is a synchronous thread and you will be blocking the main thread until completion of tasks in a group thus blocking the UI.

func downloadPhotos(withCompletion completion: ((_ error: NSError?) -> Void)?) {
DispatchQueue.global(qos: .userInitiated).async {
var storedError: NSError?
let dispatchGroup = DispatchGroup()
for address in [PhotoURLString] {
let url = URL(string: address)
dispatchGroup.enter()
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
dispatchGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
}
}
** Call enter() to manually notify the group that a task has started. You must balance out the number of enter()calls with the number of leave() calls or your app will crash.

Extra readings:
RunLoops — Runloop is associated with threads. Each thread object has a RunLoop object automatically created for it.
RunLoop objects receive events to be processed by a thread in order. RunLoops put a thread to sleep when the thread to idle and thus saving CPU usage and thus saving battery life.

References:

https://developer.apple.com/videos/play/wwdc2015/718/

https://medium.com/swift-india/parallel-programming-with-swift-part-1-4-df7caac564ae

https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2