基于自 raywenderlich.com 在2015年的两篇文章 Grand Central Dispatch Tutorial for Swift 3: Part 1/2 和 Part 2/2 以及WWDC14_716_716_What's new in GCD and XPC 做的笔记。
苹果自家出的 Concurrency Programming Guide 对 GCD 介绍得非常详细,虽然文档内容有点过时,但依然是最好的入门文档,强烈推荐。
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 也可以采用后者一样的方式,这取决于系统。
- 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 唯一能保证的是加入的任务的执行顺序是按照它们加入的时间来的。
Dispatch Queue 优选
- 预定义 Queue
系统提供了5种级别的 Dispatch Queue。
其中 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。
关于 QOS 的具体解释,可以查询这篇文档 Prioritize Work with Quality of Service Classes。
更新 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, attr: dispatch_queue_attr_t!)
serial queue 和 concurrent queue 都支持,通过后面的参数 attr 来指定,可选参数:
DISPATCH_QUEUE_SERIAL
DISPATCH_QUEUE_CONCURRENT
在 iOS 4.3之前,还不支持自定义 concurrent queue,参数 attr 只能使用0或 NULL,在一些旧的文章中该参数经常使用0或是 NULL,在 stackoverflow 上经常看到这种写法,文章中郑重指出了这是一种过时的写法,严重缺乏可读性;参数 label 是个指针,关于 UnsafePointer ,可以看这篇文章 OneVcat 的 书节选:UnsafePointer (打个广告,喵大的这本书是 iOS 中文书籍里值得购买的一本)。一般来讲就是使用 DNS 风格的字符串,类似"com.seedante.serialqueue"
这种。这个参数主要用处在于调试时便于鉴别,起个标签的作用。
在 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)
,基本上可以把它俩对等。
attributes
参数:DispatchQueue.Attributes
只有.concurrent
,没有.serial
,解释在这里:Add missing attribute option to DispatchQueue,简单来说,有了.serial
后,attributes 参数为[.serial, .concurrent]
的话就无法执行了,所以,想要 concurrent,就明确指定,而如果要生成 serial queue,又需要指定其它参数,这个参数给个[]
就可以了。在 iOS 10 后,DispatchQueue.Attributes
添加了一个类性值:initiallyInactive
,用这个选项创建的队列在提交 Block 使用前必须先激活(用activate()
),这个参数的意义是在于能够让队列再次选择目标队列,这个和最后一个参数有关。
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_once 常用于实现单例模式,单例模式有个重大缺陷:无法保证线程安全。单例模式的线程安全有两种情况需要考虑:实例的初始化过程以及读写过程。得益于 swift 对于安全理念的贯彻,第一个问题得到了解决;而后者得无法保证。举个栗子:某全局变量是个类实例,在多个线程中对其内部数据进行读写时无法保证数据的同步,软件开发中经典的读写问题。怎么解决这个问题,GCD 提供了一个优雅的方案:dispatch barriers,相关函数:
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 执行时是唯一执行的任务,如下图所示。
在 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 用于调试,然而,我看不懂,觉得总结不出个啥来。-
Semaphores(俗称信号量)
作者称 Semaphores 是 old-school threading concept,也非常复杂。我也只在有关 Unix 的文章中看到这个词。我第一次使用这个还是为了将 ALAssetsLibrary 的异步队列变成同步队列,那时只是搜索来的一个答案,完全不理解。
Semaphores 用来管制访问有限资源的任务数量。文章中的例子说实话示范作用不大,还是官方的这个例子好,Using Dispatch Semaphores to Regulate the Use of Finite Resources,这里的代码还是使用 Objective-C 写的。// 起初总是不懂这里的初始值怎么设定,好多例子写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);
在实际上好像运用比较多的地方是将异步函数变成同步函数,我当初就是这么用的,比较典型的就是这种:How do I wait for an asynchronously dispatched block to finish?,原理就是将信号量设为0,然后当前线程一直等待,直到异步的函数执行完毕发出信号,当前线程才结束等待,效果等同本来会立即返回的异步函数会同步地执行直到结束。
差不多就是这些,文章里还有用 XCTest 框架来测试异步代码的内容,看看就好。接下来,可以看看《NSOperation and NSOperationQueue Tutorial in Swift》。这里还有篇《iOS 并发编程之 Operation Queues》 值得一看。
参考链接:
1.Grand Central Dispatch Tutorial for Swift: Part 1/2
2.Grand Central Dispatch Tutorial for Swift: Part 2/2
3.WWDC14_716_716_What's new in GCD and XPC
4.Concurrency Programming Guide