GCD 原理+API 分析+实践

文章目录
  1. 1. 主要原理
  2. 2. 分发队列
    1. 2.1. 队列的创建
    2. 2.2. 创建队列并设置优先级
  3. 3. GCD 任务调度
    1. 3.1. 只执行一次 task
    2. 3.2. 添加栅栏函数
    3. 3.3. 添加 group 通知
    4. 3.4. 挂起/恢复队列
    5. 3.5. dispatch_apply进行快速迭代
    6. 3.6. Dispatch Semaphore

GCD 是操作系统层级的概念,他给用户提供了操作线程的 API
任务派发中心,内部实现原理是有了 FIFO 分发队列,GCD 源码
使用 GCD 用户不需要直接操作繁琐的 thread,线程池由系统管理,用户只需要维护分发队列,向分发队列中放 task 就可以了。
直接使用线程可能会引发的一个问题是,如果你的代码和所基于的框架代码都创建自己的线程时,那么活动的线程数量有可能以指数级增长,每个线程都会消耗一些内存和内核资源。

主要原理

主要理解三个概念

  1. queue: 管理任务的队列,确定任务派发方式
  2. task: 用户自定义需要执行的 task (代码段)
  3. thread: GCD 根据 queue 定义的方式,将 task 派发给线程池中的指定线程(thread 不需要自己创建)

GCD Queue task thread

  • queue
    • 串行队列,所有任务都在同一个 thread 上一个接着一个的执行
      • 特例:主队列 main_queue,所有 task 在 mainThread 中执行
    • 并发队列,任务可以在多个 thread 中执行没有固定执行顺序
      • 特例:global 队列
1
2
3
4
5
6
7
concurrent
adj. adj. 并发的;一致的;同时发生的;并存的
n. [数] 共点;同时发生的事件

对于并行跟并发
并发:值得是程序上多thread并发执行,其实是多线程抢占资源 -- cpu 执行(任务是以在单核 CPU 上分时(时间共享))
并行:是硬件上,多个thread在多个 cpu 上同事执行着
  • 同步异步执行任务

    • sync 所有任务要在一个 thread 中执行,一个接着一个
    • async 具备开启线程的能力,可以在多个thread 中并发执行任务
  • thread

    • 使用 GCD,不用再直接跟线程打交道了,只需要向队列中添加代码块(task)即可,GCD 在后端管理着一个线程池。
    • 作为开发者可以将工作考虑为一个队列,而不是一堆线程,这种并行的抽象模型更容易掌握和使用。
    • 根据👆图 task 真正的并发只有 右下角的才能出现,并发队列中异步执行任务
      P.S.:async 执行 task的时候 thread 的个数跟 task 的个数没有关系(现在 iOS系统是开辟 6个 thread,以前低版本的是 3个,thread 开辟太多,会用掉大量内存)

接下来说一些 GCD 接口,根据接口分析原理

分发队列

队列的创建

  1. GCD 公开有 5 个不同的全局队列:
    • 运行在主线程中的 main queue(串行队列)
    • global 队列,3 个不同优先级的后台队列(并发队列)
    • 以及一个优先级更低的后台队列(用于 I/O)(并发队列)

得到系统的全局队列

1
2
3
4
5
6
7
8
dispatch_queue_main_t mainDispatchQueue = dispatch_get_main_queue();
/*
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
*/
dispatch_queue_global_t globalDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  1. 自定义队列:串行或者并行队列。自定义队列非常强大,在自定义队列中被调度的所有 block 最终都将被放入到系统的全局队列中和线程池中。
1
2
3
dispatch_queue_t queue
concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);

创建队列并设置优先级

  1. 获取全局 global 系统队列时传递优先级
  2. dipatch_queue_attr_make_with_qos_class
  3. dispatch_set_target_queue

使用 dispatch_queue_attr_t 属性设置优先级

1
2
3
4
dispatch_queue_t queue;
dispatch_queue_attr_t attr;
attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
queue = dispatch_queue_create("com.example.myqueue", attr);
DISPATCH_QUEUE_PRIORITY_HIGHQOS_CLASS_USER_INITIATED
DISPATCH_QUEUE_PRIORITY_DEFAULTQOS_CLASS_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOWQOS_CLASS_UTILITY
DISPATCH_QUEUE_PRIORITY_BACKGROUNDQOS_CLASS_BACKGROUND
  • QOS_CLASS_USER_INTERACTIVE 指定为该QOS class的队列负责执行与用户交互相关的任务,比如动画、事件处理、更新UI等,所以有最高优先级。该优先级的队列应该只限于做与用户交互相关的任务,所以在上面优先级的宏定义中并没有将其暴露出来。(推测主队列优先级是这个)
  • QOS_CLASS_USER_INITIATED 指定为该QOS class的队列用来执行那些会阻碍用户使用你的App的任务,所以优先级也很高。
  • QOS_CLASS_DEFAULT 默认优先级。
  • QOS_CLASS_UTILITY 指定为该QOS class的队列用于执行那些用户不需要立即得到结果的任务,所以优先级相对较低。(long-running computations, I/O, networking or continuous data feeds.)
  • QOS_CLASS_BACKGROUND 指定为该QOS class的队列用于执行维护或清理等任务,用户不需要关心其结果。

设置目标队列

dispatch_set_target_queue 相关注释说明

1
2
3
4
5
6
* When no quality of service class and relative priority is specified for a
* dispatch queue at the time of creation, a dispatch queue's quality of service
* class is inherited from its target queue. The dispatch_get_global_queue()
* function may be used to obtain a target queue of a specific quality of
* service class, however the use of dispatch_queue_attr_make_with_qos_class()
* is recommended instead.
  1. 将自定义 queue 中的 task 都将被放入到系统的全局队列和线程池中:默认情况下会把开发者创建的队列放入到默认优先级的全局队列中。但是也可以给自定义的队列设置一个目标队列
  2. 改变自定义 queue 的优先级:让其执行优先级与该目标队列的执行优先级一致。
  3. 改变 queue 中 task 执行的方式:不仅能改变优先级,如果一个队列是并行的,但是其目标队列是串行的,那么实际上这个队列也会转换为串行队列。再者,不同串行队列中的任务是可以同时执行的,如果把这些串行队列的目标队列都设置为同一个串行队列,那这些串行队列中的任务将不会并行执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
 * @param object
* The object to modify.
* The result of passing NULL in this parameter is undefined.
*
* @param queue
* The new target queue for the object. The queue is retained, and the
* previous target queue, if any, is released.
* If queue is DISPATCH_TARGET_QUEUE_DEFAULT, set the object's target queue
* to the default target queue for the given object type.
*/
DISPATCH_EXPORT DISPATCH_NOTHROW
void
dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t _Nullable queue);

dispatch_set_target_queue:可以设置优先级,也可以设置队列层级体系,比如让多个串行和并行队列在统一一个串行队列里串行执行

dispatch_set_target_queue使用后的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func setTarget() {
let serialQueue = DispatchQueue(label: "serialQueue")
let aQueue = DispatchQueue(label: "concurrent.queue.0", attributes: .concurrent, target: serialQueue)
let bQueue = DispatchQueue(label: "concurrent.queue.1", attributes: .concurrent, target: serialQueue)

let alabel = aQueue.label
let blabel = bQueue.label
aQueue.async {
print("\(alabel): 1")
sleep(3)
}

bQueue.async {
print("\(blabel): 2")
sleep(1)
}

bQueue.async {
print("\(blabel): 3")
sleep(2)
}
}
/*
concurrent.queue.0: 1
concurrent.queue.1: 2
concurrent.queue.1: 3
*/
func nosetTarget() {
let aQueue = DispatchQueue(label: "concurrent.queue.0", attributes: .concurrent)
let bQueue = DispatchQueue(label: "concurrent.queue.1", attributes: .concurrent)

let alabel = aQueue.label
let blabel = bQueue.label
aQueue.async {
print("\(alabel): 1")
sleep(3)
}

bQueue.async {
print("\(blabel): 2")
sleep(1)
}

bQueue.async {
print("\(blabel): 3")
sleep(2)
}
}
/*
concurrent.queue.0: 1
concurrent.queue.1: 3
concurrent.queue.1: 2
*/

GCD 任务调度

1
2
3
4
5
6
7
8
9
10
public func sync(execute workItem: DispatchWorkItem)
public func async(execute workItem: DispatchWorkItem)
public func async(group: DispatchGroup, execute workItem: DispatchWorkItem)
public func async(group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)
public func sync<T>(execute work: () throws -> T) rethrows -> T
public func sync<T>(flags: DispatchWorkItemFlags, execute work: () throws -> T) rethrows -> T
public func asyncAfter(deadline: DispatchTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)
public func asyncAfter(wallDeadline: DispatchWallTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)
public func asyncAfter(deadline: DispatchTime, execute: DispatchWorkItem)
public func asyncAfter(wallDeadline: DispatchWallTime, execute: DispatchWorkItem)

只执行一次 task

dispatch_once 只执行一次指定的block。它的性能要比@synchronized要好。@synchronized每一次都要先获取锁,而dispatch_once使用一个token标识代码是否执行过。

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

});

在Swift 3.0中这个函数被废弃了,但是可以使用懒加载的全局变量或静态变量,也能保证线程安全。

1
2
3
let onceTask: String = {
return "onceTask"
}()

添加栅栏函数

Dispatch Barrier解决多线程多读单写同一个资源发生死锁问题
同步队列中不会出现这个问题
在并发队列中,如果添加了 .barrier task 那么在 .barrier task 之前添加的任务所有任务执行之前都会有任务执行,在全局并发队列和串行队列上,效果和dispatch_sync一样

barrier

有两个接口

1
2
3
4
5
6
7
8
9
10
//Submits a barrier block object for execution and waits until that block completes.
//The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_sync function.
dispatch_barrier_sync(queue, ^{

})

//Submits a barrier block for asynchronous execution and returns immediately.
dispatch_barrier_async(queue, ^{

})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func barrier() {
let q = DispatchQueue(label: "barrier", attributes: .concurrent)
q.async {
print(1)
}
q.async(flags: .barrier) {
print("sleep 2s----")
sleep(2)
}
q.async {
print(3)
}
q.async {
print(4)
}
}

在实际开发中一个很好的使用 barrier 控制多读单写的例子,就是重写 getter,setter 方法
多读单写特点:

  • 读者与读者并发
  • 读者与写者互斥
  • 写者与写者互斥

多读单写

  1. 要在 concurrent queue 中执行
  2. dispatch_barrier_async
  3. 重写需要加锁数据的 getter,setter 方法
    1. getter 方法需要立即得到数据所以使用 sync
    2. setter 方法 async ,因为读的过程不需要立即得到结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对于变量
let rwQueue = DispatchQueue(label: "multireadsinglewrite", attributes: .concurrent)
var test_ = 0
var test: Int {
get {
rwQueue.sync {
return test_
}
}
set {
rwQueue.async(flags: .barrier) {
self.test_ = newValue
}
}
}

添加 group 通知

dispatch groups是专门用来监视多个异步任务。dispatch_group_t实例用来追踪不同队列中的不同任务。

当group里所有事件都完成GCD API有两种方式发送通知
第一种是dispatch_group_wait,会阻塞当前进程,等所有任务都完成或等待超时
第二种方法是使用dispatch_group_notify,异步执行闭包,不会阻塞。

1
2
3
4
5
6
7
8
9
// notify 任务组完成后,执行 notify 要做的事,notify不阻塞线程
public func notify(qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], queue: DispatchQueue, execute work: @escaping @convention(block) () -> Void)
@available(OSX 10.10, iOS 8.0, *)
public func notify(queue: DispatchQueue, work: DispatchWorkItem)

// 等待直到完成or超时
public func wait()
public func wait(timeout: DispatchTime) -> DispatchTimeoutResult
public func wait(wallTimeout timeout: DispatchWallTime) -> DispatchTimeoutResult

如果放入组里面的任务内部没有嵌套任务,那么一切正常
可是如果组里面的 task 内部异步任务呢?eg task0{ async(task1) }
这个时候我想,task1 异步回调执行完,才算task0 这个组任务完成该怎么做?

1
2
public func enter()
public func leave()

实际使用group的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
let downloadGroup = DispatchGroup()
for url in [url0,url1,url2] {
downloadGroup.enter()
let photo = DownloadPhoto(url: url) { _, error in
if error != nil { }
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}

downloadGroup.notify(queue: DispatchQueue.main) {
print("finish")
}

挂起/恢复队列

1
2
queue.suspend()// 当前 isExcuting = true 的operation 不会听通知
queue.resume()

这里挂起不会暂停正在执行的block

dispatch_apply进行快速迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func apply() {
let q = DispatchQueue(label: "apply", attributes: .concurrent)
__dispatch_apply(5, q){ idx in
print(idx)// 异步并发
}
print("apply end")// 同步等待
}
/*
0
4
1
3
2
apply end
*/

dispatch_apply

  • 在并发队列中使用
  • 将 task 追加到队列中
  • 所有 task 并发执行完以后同步执行后面的打印
  • dispatch_apply 添加的 task 是异步并发执行,外部是同步执行

apply

用dispatch_apply替代对数组等的for循环,把这些block放到并行队列中可以提高执行效率。

1
P.S. apply 主要针对于大量并发的时候使用,少量的时候没有必要

Dispatch Semaphore

使用变量管理多线程的同步方法

1
2
3
dispatch_semaphore_t dispatch_semaphore_create(long value);
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);

并发编程:API 及挑战
细说GCD(Grand Central Dispatch)如何用