Introduction

When we use network requesting, always use escaping closure to resolve the data passed from net to our UI, though it generally works, but when getting into some intricate cases, we need to use nested clousure, which is not elegant, so study Threading Control Tech is useful for us to code better.

For example, we need to request a Image from internet, we can request data in the background, and allow UI moves quickly in the main thread, after fetched data, refresh the Image Control in main thread.

What Is GCD

From Swift3 we really step into the world of Multi Threading Coding, where we have elegant grammer unlike the infrastructure developing with C in the past, Apple provided brand new grammer for us. However that is 3 or 4 years ago, now we just use several lines we can complete our tasks.

GCD is the logogram of Grand Central Dispatch, the main part of it is Dispatch Queue. Queue is an object that for we to let tasks to run sync or async.

DispatchQueue

Before knowing what is sync and async, I have to introduce a concept calls Dispatch Queue, which is called as Queue before, first we should import Foundation, and initialize a queue waiting for task.

1
let queue = DispatchQueue(label: "zrzz.site")

This is a customed queue that runs tasks serialy, we can give .concurrent to the ATTRIBUTE “attributes” to make it run tasks concurrently. Like this.

1
let queue = DispatchQueue(label: "zrzz.site", attributes: .concurrent)

The basic method of initialize DispatchQueue has one parameter, label is decided by you, as long as it is unique.

global and main

1
DispatchQueue.global()/DispatchQueue.main

global queue can runs tasks concurrently

main queue is relative to main thread whick controls our UI update, also it is serial.

Sync Async

Sync and Async, as its literal meaning, we can make our queue run two tasks synchronously or asynchronously. To realize it better, plz run codes below and observe results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Foundation

let queue = DispatchQueue(label: "zrzz.site")
//let queue = DispatchQueue(label: "zrzz.site", attributes: .concurrent)
let start = Date().timeIntervalSince1970

for i in 0..<5 {
queue.sync {
print("Task - \(i) \(Thread.current) \(Thread.isMainThread)")
}
}
for i in 0..<5 {
print("Task - \(i) \(Thread.current) \(Thread.isMainThread)")
}
let end = Date().timeIntervalSince1970
print(end - start)

image-20210320193813647

We create one queue, let it syncly run a for loop, and let main thread (outside the closure) use sync method, and Cpu exec our tasks by order.

We can see the queue doesn’t create new thread to handle tasks, but still let them runs in the main thread.

What if we use async method? Let’s check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Foundation

let queue = DispatchQueue(label: "zrzz.site")
//let queue = DispatchQueue(label: "zrzz.site", attributes: .concurrent)
let start = Date().timeIntervalSince1970

for i in 0..<5 {
queue.async {
print("Task - \(i) \(Thread.current) \(Thread.isMainThread)")
}
}
for i in 0..<5 {
print("Task - \(i) \(Thread.current) \(Thread.isMainThread)")
}
let end = Date().timeIntervalSince1970
print(end - start)

image-20210320193709326

As expected, two tasks run asyncly, tasks in the clousure runs in a new thread.

So let’s conclude async and sync with serial and concurrent queue.

sync + serial

won’t create new thread, runs in order.

async + serial

create new thread, runs in order.

sync + concurrent

won’t create new thread, runs in order

async + concurrent

create new thread every time (useless thread will be recycled, don’t worry about it), runs randomly.

Addition

however, you CAN’T use sync in main Queue, because main Queue is used for UI update, and the action SYNC will block the thread, which is not allowed in Main Thread.

if you use async + mainQueue, it won’t create thread and runs in order. And there is a interesting action if you run the code below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foundation

//let queue = DispatchQueue(label: "zrzz.site")
let queue = DispatchQueue(label: "zrzz.site", attributes: .concurrent)
let start = Date().timeIntervalSince1970

for i in 0..<5 {
queue.async {
print("Task - \(i) \(Thread.current) \(Thread.isMainThread)")
}
}
for i in 0..<5 {
DispatchQueue.main.async {
print("MainTask - \(i) \(Thread.current) \(Thread.isMainThread)")
}
}
for i in 0..<5 {
print("Task - \(i) \(Thread.current) \(Thread.isMainThread)")
}
let end = Date().timeIntervalSince1970
print(end - start)

you can see the tasks not runs as expected. I think the reason is that tasks in async closure was added to the end of queue.

Barrier

if we want some tasks run first and other behind them, we can use barrier. Like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Foundation

//let queue = DispatchQueue(label: "zrzz.site")
let queue = DispatchQueue(label: "zrzz.site", attributes: .concurrent)

for i in 0..<5 {
queue.async {
print("Task - \(i) \(Thread.current) \(Thread.isMainThread)")
}
}

queue.async(flags: .barrier) {
print("0..<5 before, 5..<10 behind")
}

for i in 5..<10 {
queue.async {
print("Task - \(i) \(Thread.current) \(Thread.isMainThread)")
}
}

image-20210320195526315

Quality of Service (QoS)

However, we usually want to do things concurrently, and we can create different queues for them. And use qos to control priority.

DispatchQos is ordered by below.

  • userInteractive
  • userInitiated
  • default
  • utility
  • background
  • unspecified
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Foundation

let queue1 = DispatchQueue(label: "q1", qos: .userInteractive)
let queue2 = DispatchQueue(label: "q2", qos: .default)

for i in 0..<5 {
queue1.async {
print("Task1 - \(i) \(Thread.current) \(Thread.isMainThread)")
}
}

for i in 0..<5 {
queue2.async {
print("Task2 - \(i) \(Thread.current) \(Thread.isMainThread)")
}
}

image-20210320200231783

we can see tasks in queue2 always behind that in queue1.

DispatchGroup

Group can be regarded as an tasks set. It’s normol usage is to know when the tasks were done.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Foundation

let queue = DispatchQueue(label: "zrzz.site", attributes: .concurrent)
let group = DispatchGroup()

for i in 0..<10 {
queue.async(group: group) {
print(i)
}
}

group.notify(queue: queue) {
print("all tasks done")
}

or you can manually control tasks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Foundation

let queue = DispatchQueue(label: "zrzz.site", attributes: .concurrent)
let group = DispatchGroup()

for i in 0..<10 {
group.enter()
queue.async(group: group) {
print(i)
group.leave()
}
}

group.notify(queue: queue) {
print("all tasks done")
}

Semaphore

use semaphore to limit the count of tasks that running at the same time.

1
2
3
4
5
6
7
8
9
10
11
import Foundation

let semaphore = DispatchSemaphore(value: 2)

for i in 0..<10 {
semaphore.wait()
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
print("Task - \(i) \(Thread.current)")
semaphore.signal()
}
}