GCD 在 Swift 中的 API 随着 Swift 的版本变化很大,从 Swift 3 开始完全对象化了,不过 API 的语法好像到 Swift 3.2 才稳定,不过还有个很头疼的问题是新 API 的官方文档一直缺失,都好几年了,建议去 Objective-C 版本的头文件里看对应的文档,非常详细。
基本概念
- Serial vs. Concurrent
这两个词用来描述执行多个任务时任务之间的关系:Serial,常译作「串行」,表示这些任务同时最多只能有一个任务在执行;Concurrent,在这种语境下常译作「并行」,表示这些任务有可能同时执行多个。 - Synchronous vs. Asynchronous
这两个词用来描述函数返回的时机以及函数的运作方式:Synchronous 常译作「同步」,表示函数占用当前线程直到运行结束才返回结果;Asynchronous 常译作「异步」,表示函数立即返回结果,而把实际的任务放在其他线程里运行。 -
Concurrency vs. Parallelism
两者的区别在于,前者需要进行上下文切换造成同时执行两个或多个线程的假象。Parallelism 在多核设备上才能进行,而得益于多核,Concurrency 也可以采用后者一样的方式,这取决于系统。
Concurrency vs. Parallelism - GCD and Queue
GCD 全称 Grand Central Dispatch, 是 libdispatch 这个库的外部代号,它提供 dispatch queues 来执行任务;dispatch queue 都是线程安全的,并且保证加入的任务按照 FIFO 规则来运行;dispatch queue 无法保证任务的精确执行时间,需要控制执行时间就直接使用 NSThread;dispatch queue 分为 serial queue 及 concurrent queue 两类,与第1点的概念匹配。 - Serial Queues vs. Concurrent Queues
serial queues 保证一次只执行一个任务,当前任务结束后才能执行下一个任务,但不保证两个任务之间的间隔时间;concurrent queue 唯一能保证的是加入的任务的执行顺序是按照它们加入的时间来的。
Concurrent Queue
Dispatch Queue 优选
- 预定义 Queue
系统提供了5种级别的 Dispatch Queue。
GCD Queues其中 main queue,也就是用于 UI 更新的 queue,是个 serial queue, 可以通过
DispatchQueue.main
获取;剩下的是不同优先级的全局并发队列 concurrent queue,通过DispatchQueue.global(qos: DispatchQoS.QoSClass>)
获取,DispatchQoS.QoSClass
,就是以往OC 中 Dispatch Queue Priorities 在 Swift 中的表示,该参数的默认值是.default
,也就是 DISPATCH_QUEUE_PRIORITY_DEFAULT。
Swift QOS map to Objective-C DISPATCH_QUEUE_PRIORITY
更新 UI 切记一定要在 main queue 里进行,不然很有可能跟你的预期不一样。想我还是个大菜鸟的时候,就犯了这个错误,死活找不到原因。在 Xcode 9 里有了 Main Thread Checker 可以检测到不在主线程更新 UI 的代码,貌似默认是开启的,在 Edit Scheme -> Run -> Disgnostics 里。
- 自定义 Queue
系统提供的唯一的 serial queue 是 main queue,为了不阻塞 main queue,就需要自制 serial queue 了。
在 Objective-C 中通过以下函数来获取自定义 queue:
dispatch_queue_create(label: UnsafePointer<Int8>, attr: dispatch_queue_attr_t!)
serial queue 和 concurrent queue 都支持,通过后面的参数 attr 来指定,可选参数:
DISPATCH_QUEUE_SERIAL
DISPATCH_QUEUE_CONCURRENT
在 Swift 中这样来获取自定义 serial queue:
let serialQueue = DispatchQueue.init(label: "CustomSerialQueue")
实际上这个函数的原型相当复杂,有好几个配置项,除了 label 参数其它的都有默认值:
init(label: String, qos: DispatchQoS, attributes: DispatchQueue.Attributes, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency, target: DispatchQueue?)
获取一个 concurrent queue 则需要在 attributes 里明确指定:
let concurrentQueue = DispatchQueue.init(label: "CustomCQ", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
qos
参数:这里的DispatchQoS
与上面的DispatchQoS.QoSClass
差不多,只是多了一个考量因素relativePriority(Int)
,基本上可以把它俩对等。
autoreleaseFrequency
这个参数比较费解,查看头文件得知这个参数用于指定如何利用 autorelease pool 处理提交的 block 的内存,三个预定义值的解释如下:
.inherit: 继承目标队列的处理策略,是手动创建的 queue 的默认处理方式。
.workItem: 每个 block 执行前自动创建,执行完毕后自动释放。
.never: 不会为每个 block 单独设立 autorelease pool,这是全局并发队列的处理方式。
除了.inherit
,剩下的两个都是 iOS 10 以上才能用。
最后的参数target
让我摸不着头脑,既然已经有了qos: DispatchQoS
,这个不是多此一举吗?我很难理解为队列提供目标队列这个设计的作用,这个设计可以溯源至DispatchQueue
的父类DispatchObject
:
DispatchObject.setTarget(queue: DispatchQueue?)
目标队列为 DispatchObject 执行任务代码,这个方法的文档里提到可以为 Dispatch sources 和 Dispatch I/O channels 提供执行任务代码的目标队列,这两者自身没有线程可用,所以需要依赖目标队列。在 Objective-C 中,手动创建的队列可能没有指定 priority,设定目标队列勉强还有那么点意义。
另外,这个方法有个 Bug: 如果你希望将目标队列设置为.default
的全局队列,要明确指定DispatchQueue.global(qos: .default)
,而不能使用DispatchQueue.global()
,尽管这两个是等价的。
常规使用
- 任务封装 Dispatch Block 和 DispatchWorkItem
dispatch_block_t 得到了强化,添加了多个功能:
- 等待完成,可以指定等待时间
- 完成通知,和上一个功能合起来看如同 DispatchGroup,连 API 都一样
- 执行前取消
- Qos
在 Objective-C 中,由于自定义的 queue 可能没有指定 priority, target queue 也可能没有指定,这次给 dispatch_block_t 加上了 QoS 来提供最后的默认选择。 - flags
为 Block 的执行增加了一些配置项目,效果类似于convenience init
,实在懒得写了,这个的文档没有缺失。
这些新东西在 Swift 的对应就是DispatchWorkItem
类,在 Swift 中提交到 queue 的 block 自动被封装成了DispatchWorkItem
。
- 在 Dispatch Queue 里执行任务
有了 dispatch queue,还需要正确的执行方式,GCD 日用五大金刚:
dispatch_async
dispatch_sync: 这个方法会尽可能地在当前线程执行 Block
dispatch_after
dispatch_apply:class func concurrentPerform(iterations: Int, execute work: (Int) -> Void)
dispatch_once: 在 Swift 中已移除
前三个方法已经转化为DispatchQueue
的实例方法,dispatch_apply
则成了类方法
dispatch_barrier_async(queue: dispatch_queue_t, block: dispatch_block_t)
dispatch_barrier_sync(queue: dispatch_queue_t, block: dispatch_block_t)
GCD barrier 保证提交的 block 是指定的 queue 中在该 block 执行时是唯一执行的任务,如下图所示。
Dispatch Barrier
在 Swift 中,实现单例模式已经非常简单,使用 let 就可以了。
dispatch_apply 就是 concurrent 版本的 for loop,因此,dispatch_apply 必须放在 concurrent queue 中执行。for loop 每次 iteration 执行一个任务,而 dispatch_apply 则是将所有 iteration 的任务并行执行,所有任务完成后才返回,因此,dispatch_apply 同时也是 synchronous 的。在 Swift 中,这个 API 是如下形式:
class func concurrentPerform(iterations: Int, execute work: (Int) -> Void)
iterations
代表并发的数量,work
闭包里的 Int 参数起着 Index 的作用。
其它
- Dispatch Group
DispatchGroup 能够追踪多个任务的完成,支持多个 queue。
func dispatchGroupDemo(){
let queueGroup = DispatchGroup.init()
let serialQueue = DispatchQueue.init(label: "CustomSerialQueue")
let concurrentQueue = DispatchQueue.init(label: "CustomCQ", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
serialQueue.async(group: queueGroup, execute: DispatchWorkItem.init(block: {
queueGroup.enter()//告知 block 开始执行
NSLog("Group block 0 begin")
sleep(arc4random_uniform(UInt32(8)))
NSLog("Group block 0 over")
queueGroup.leave()//告知 block 已经完成了
}))
concurrentQueue.async(group: queueGroup, execute: DispatchWorkItem.init(block: {
queueGroup.enter()
NSLog("Group block 1 begin")
sleep(arc4random_uniform(UInt32(6)))
NSLog("Group block 2 over")
queueGroup.leave()
}))
// 等待指定的时间,如果到了指定的时间跟踪的 block 并没有全部完成则返回 .timeout
// 可以使用wait()一直等待直到跟踪的所有 block 完成
let waitResult = queueGroup.wait(timeout: .now() + 5)
NSLog("All tasks are completed in 5s: \(waitResult)")
}
DispatchGroup 也支持异步的等待,在跟踪的所有 block 完成后得到通知,并在指定的队列里执行代码。
func notify(queue: DispatchQueue, work: DispatchWorkItem)
-
Dispatch Source
Dispatch Source 用来监视一些系统底层事件并自动做出反应:在 dispatch queue 中提交 Block 对事件作出处理,感觉很熟悉是吧。我还没处理底层的经验,文章使用的例子是利用 dispatch source 对应用恢复运行状态做出反应,然而还是不懂这个的用处。作者表示为了在现实中能派上用场利用 dispatch source 实现了一个 stack trace tool 用于调试,然而,我看不懂,觉得总结不出个啥来。 -
// 起初总是不懂这里的初始值怎么设定,好多例子写0,也不懂含义。实际上,这个初始值是代表着可访问资源的数量,意义在后面体现。这里的数量表示程序同时最多可以打开的文件数量,限制这个数量避免性能问题。 dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2); // 这行代码写在这里让人疑惑,在实际中,这行代码可能在不同的线程里运行,这样就好理解了。wait 函数将信号量的数量减1,如果此时信号量的值小于0了,表示当前资源不足,不可访问;这里又将超时时间设定为一直等待,那么会一直等下去,同其他等待的线程一起按照 FIFO的规则排队;或信号量的值大于0,代表还有可用资源,可以访问,代码继续往下运行,程序打开一个文件,同时函数返回0表示成功, dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER); fd = open("/etc/services", O_RDONLY); // 处理完毕,关闭文件。然后,dispatch_semaphore_signal()将信号量加1,表示可访问资源加1,发出信号,此时正在等待访问该资源的其他线程将继续竞争访问。 close(fd); dispatch_semaphore_signal(fd_sema);