Grand Central Dispatch简称GCD,是苹果公司为多核的并行运算提出的解决方案, 允许程序将任务切分为多个单一任务然后提交至工作队列来并发地或者串行地执行。GCD会自动利用更多的CPU内核(比如双核、四核), 自动管理线程的生命周期(创建线程、调度任务、销毁线程).
下面逐一介绍DispatchQueue, Operation和OperationQueue.
文中的示例代码均可参见我的GitHub: https://github.com/zhihuitang/GCDExample
1. DispatchQueue
GCD的基本概念就是DispatchQueue。DispatchQueue是一个对象,它可以接受任务,并将任务以FIFO(先到先执行)的顺序来执行。DispatchQueue可以是并发的或串行的, 它有3种队列类型:
- Main queue
- Global queue
- Custom queue
1.1 Main queue(串行Serial)
Main queue运行在系统主线程.它是一个串行队列,我们所有的UI刷新都发生在Main queue. 获取Main queue的方法很简单:
// Get the main queue
let mainQueue = DispatchQueue.main
如果要在非Main queue线程中直接刷新UI, 运行时会出exception. 一般的做法是将代码放在Main queue中异步执行:
let mainQueue = DispatchQueue.main
mainQueue.async {
// UI刷新代码在这里
}
1.2 Global queues(并行Concurrent)
如果要在后台执行非UI相关的工作, 一般把这部分工作放在Global queue. Global queue是一种系统内共享的并行的队列. 申请Global queue的方法很简单:
// Get the .userInitiated global dispatch queue
let userQueue = DispatchQueue.global(qos: .userInitiated)
// Get the .default global dispatch queue
let defaultQueue = DispatchQueue.global()
Global queue队列有四种优先级: 高, 缺省, 低, 后台. 在实际申请Global queue时,我们不需直接指定优先级, 只需申明所需的(QoS)类型, Qos间接的决定了这些queue的优先级
例如:
DispatchQueue.global(qos: .userInteractive)
DispatchQueue.global(qos: .userInitiated)
DispatchQueue.global() // .default qos
DispatchQueue.global(qos: .utility)
DispatchQueue.global(qos: .background)
DispatchQueue.global(qos: .unspecified)
The QoS classes are:
User-interactive
表示为了给用户提供一个比较好的体验, 任务必须立即完成. 主要用于UI刷新, 低延迟的事件处理等. 在整个App内, 这种类型的任务不宜太多. 它是最高优先级的.User-initiated
用于UI发起异步任务,用户在等待执行的结果, 这种queue是高优先级的.
DispatchQueue.global(qos: .userInitiated).async {
let overlayImage = self.faceOverlayImageFromImage(self.image)
DispatchQueue.main.async {
self.fadeInNewImage(overlayImage)
}
}
Utility
长时间运行的任务, 典型情况是App中会有一个进度条表示任务的进度. 主要用于 计算, I/O, 网络交互等, 主要为节能考虑. 这种queue是低优先级的.Background
任务在运行, 但用户感觉不到它在运行的场景. 主要用于不需要用户干涉,对时间不敏感的获取数据等任务, 这种queue是后台优先级,属于最低优先级的那一种.
1.3 Custom queues
用户创建的Custom queues默认的是串行的, 如果指定了attributes为concurrent则为并行的. 下面我们用代码演示串行队列/并行队列的区别.
- Serial Queue
除了DispatchQueue.main是并行的queue外, 你也可以创建自己的并行queue(缺省为串行)
func task1() {
print("Task 1 started")
// make task1 take longer than task2
sleep(1)
print("Task 1 finished")
}
func task2() {
print("Task 2 started")
print("Task 2 finished")
}
example(of: "Serial Queue") {
let mySerialQueue = DispatchQueue(label: "com.crafttang.serialqueue")
mySerialQueue.async {
task1()
}
mySerialQueue.async {
task2()
}
}
sleep(2)
上面代码的输出为:
--- Example of: Serial Queue ---
Task 1 started
[ spent: 0.00026 ] seconds
Task 1 finished
Task 2 started
Task 2 finished
从上可以看出, task1和task2是顺序运行的, 只有task1执行完了后task2才可以执行.由于task1和task2都是异步(asyc)执行的, 所以不会阻塞当前线程, 2个任务执行的时间只有0.00026秒.
- Concurrent Queue
创建用户自己的并行queue, 声明.concurrent属性即可:
example(of: "Concurrent Queue") {
let concurrentQueue = DispatchQueue(label: "com.crafttang.currentqueue", attributes: .concurrent)
concurrentQueue.async {
task1()
}
concurrentQueue.async {
task2()
}
}
sleep(2)
上面的输出为:
--- Example of: Concurrent Queue ---
Task 1 started
Task 2 started
Task 2 finished
[ spent: 0.00034 ] seconds
Task 1 finished
从上面可以看出, task1和task2是并发执行的, task1启动后, 由于执行时间需要1s, 这个时候task2也可以同步运行, 所以我们可以看到task1启动后, 立即启动task2, 然后task2完成, task1完成.
task1和task2都是异步(async)运行的,所以它们花费的时间仍然很短, 只有0.00034秒.
同步(Synchronous) vs. 异步(Asynchronous)
对于一个任务(function), 可以在GCD队列里同步运行, 也可以异步运行.
同步运行的任务, 不开启新的线程, 会阻塞当前线程, 等任务完成才返回.
异步运行的任务, 会开启新的线程, 不会阻塞当前线程, 分发任务后立即返回,不用等任务完成.
2. DispatchGroup
DispatchGroup实例用来追踪不同队列中的不同任务。当group里所有事件都完成GCD API有两种方式发送通知,第一种是DispatchGroup.wait,会阻塞当前进程,等所有任务都完成或等待超时。第二种方法是使用DispatchGroup.notify,异步执行闭包,不会阻塞当前线程。
example(of: "DispatchGroup") {
let workerQueue = DispatchQueue(label: "com.crafttang.dispatchgroup", attributes: .concurrent)
let dispatchGroup = DispatchGroup()
let numberArray: [(Any,Any)] = [("A", "B"), (2,3), ("C", "D"), (6,7), (8,9)]
for inValue in numberArray {
workerQueue.async(group: dispatchGroup) {
let result = slowJoint(inValue)
print("Result = \(result)")
}
}
//dispatchGroup.wait(timeout: .now() + 1)
let notifyQueue = DispatchQueue.global()
dispatchGroup.notify(queue: notifyQueue) {
print(" joint tasks finished")
}
}
以上程序的输出如下:
--- Example of: DispatchGroup ---
[ spent: 0.00406 ] seconds
Result = CD
Result = AB
Result = 23
Result = 67
Result = 89
joint tasks finished
3. Operation/OperationQueue
GCD是一个底层的C API, OperationQueue是基于GCD和队列模型的一个抽象,负责Operation的调度.这意味着我们可以像GCD那样并行的执行任务, 但是以面向对象的方式.
GCD和OperationQueue主要差别如下:
- OperationQueue不遵循FIFO: 相比于GCD, OperationQueue使用起来更加灵活. 在OperationQueue中,我们可以这是Operation之间的依赖, 例如Operation-B任务只有在Operation-A执行完成之后才能开始执行. 这也是OperationQueue不遵循FIFO的原因.
- OperationQueue并行运行: OperationQueue中的任务(Operation)并行执行, 你不能设置成串行Serial. 但是可以有个变通方法达到串行运行的效果,就是设置依赖, C依赖B, B依赖A; 所以他们运行的顺序就是
A -> B -> C
- 一个独立的Operation任务是同步运行的, 如果想让Operation异步运行, 你必须将Operation加入到一个OperationQueue.
- Operation queues 是OperationQueue 的一个实例, 实际上Operation queue是被封装在一个Operation中运行的.
3.1 Operation
提交到OperationQueue中的任务必须是一个Operation实例.你可以简单的认为Operaion就是一项工作/任务. Operation是一个不能直接使用的抽象类,在实际使用中你必须用Operation的子类.
open class Operation : NSObject {
open func start()
open func main()
open var isCancelled: Bool { get }
open func cancel()
open var isExecuting: Bool { get }
open var isFinished: Bool { get }
open var isConcurrent: Bool { get }
@available(iOS 7.0, *)
open var isAsynchronous: Bool { get }
open var isReady: Bool { get }
open func addDependency(_ op: Operation)
open func removeDependency(_ op: Operation)
open var dependencies: [Operation] { get }
open var queuePriority: Operation.QueuePriority
@available(iOS 4.0, *)
open var completionBlock: (() -> Swift.Void)?
@available(iOS 4.0, *)
open func waitUntilFinished()
@available(iOS, introduced: 4.0, deprecated: 8.0, message: "Not supported")
open var threadPriority: Double
@available(iOS 8.0, *)
open var qualityOfService: QualityOfService
@available(iOS 8.0, *)
open var name: String?
}
在iOS SDK中, 苹果提供了2种Operation的可实例化子类BlockOperation
和 InvocationOperation
, 这2种类我们可以直接使用.
- BlockOperation. 对
DispatchQueue.global()
的一个封装, 它可以管理一个或多个blocks, bocks异步,并行的执行, 如果你想串行的执行任务, 你可以设置依赖或选择Custom queues(参见DispatchQueue).BlockOperation为一些已经使用了OperationQueue但不想使用DispatchQueue的App, 提供一种面向对象的封装. 作为一种Operation, 相比DisptachQueue, 它提供了更多的特性例如添加依赖, KVO通知, 取消任务等. 某种程度上BlockOperation也像一个一个DispatchGroup: 所有的blocks完成后它会收到通知. - 如果BlockOperation中只有一个任务, 那么这个任务会在当前线程中. 如果有多个任务, 那么系统可能会开启多个线程来执行这些任务
example(of: "BlockOperation") {
let blockOperation = BlockOperation()
for i in 1...10 {
blockOperation.addExecutionBlock {
sleep(2)
print("\(i) in blockOperation: \(Thread.current)")
}
}
blockOperation.completionBlock = {
print("All block operation task finished: \(Thread.current)")
}
blockOperation.start()
}
The output is as fellows:
--- Example of: BlockOperation ---
8 in blockOperation: {number = 11, name = (null)}
6 in blockOperation: {number = 1, name = main}
4 in blockOperation: {number = 7, name = (null)}
3 in blockOperation: {number = 8, name = (null)}
2 in blockOperation: {number = 9, name = (null)}
5 in blockOperation: {number = 6, name = (null)}
1 in blockOperation: {number = 10, name = (null)}
7 in blockOperation: {number = 5, name = (null)}
10 in blockOperation: {number = 1, name = main}
9 in blockOperation: {number = 11, name = (null)}
All block operation task finished: {number = 5, name = (null)}
[ spent: 4.00875 ] seconds
加入到blockOperation的每个block需要花费2s, 从输出日志可以看出, 10个blocks运行完花费了约4s. 说明这些block是并行运行的,至少有5个线程同时运行.
- InvocationOperation – 已经从Swift中移除了,无需关注
关于自定义的Operation, 下面以一个例子介绍该如何使用. 这个例子是一个Operation, 将我女朋友的照片模糊化后输出.
首先定义一个类,继承与Operation:
class BlurImageOperation: Operation {
var inputImage: UIImage?
var outputImage: UIImage?
override func main() {
outputImage = blurImage(image: inputImage)
}
}
其中blurImage(image: inputImage)
是将输入的图片模糊化, 它是一个耗时任务.
然后使用这个BlurImageOperation
:
let operation = BlurImageOperation()
operation.inputImage = inputImage
operation.start()
operation.outputImage
注意Operation启动的方式有2中, 要么手动.start()
启动,要么将Operation加入到OperationQueue中自动启动.这里我们没用OperationQueue,所以要手动启动.
Operation本身是同步执行的, 所以operation.start()
会阻塞在这里, 可能会花较长时间. operation执行完成后, 在Playground中点击operation.outputImage
对应的侧边栏上的小眼睛Quick Look可以查看处理完成的图片:
从日志输出可以看出, 总的图片处理BlurImageOperation花费了4.9s
--- Example of: Operation ---
[ spent: 4.96002 ] seconds
3.2 OperationQueue
OperationQueue负责Operation任务集的调度, Operation加入OperationQueue后, 立即启动运行, 无需手工启动. 通过maxConcurrentOperationCount
设置并发任务的数量:
// DONE: Create printerQueue
let operationQueue = OperationQueue()
// DONE later: Set maximum to 2
operationQueue.maxConcurrentOperationCount = 2
下面一个具体的例子来说明OperationQueue的用法.
example(of: "OperationQueue") {
let printerQueue = OperationQueue()
//printerQueue.maxConcurrentOperationCount = 2
printerQueue.addOperation { print("厉"); sleep(3) }
printerQueue.addOperation { print("害"); sleep(3) }
printerQueue.addOperation { print("了"); sleep(3) }
printerQueue.addOperation { print("我"); sleep(3) }
printerQueue.addOperation { print("的"); sleep(3) }
printerQueue.addOperation { print("哥"); sleep(3) }
//阻塞在这里
printerQueue.waitUntilAllOperationsAreFinished()
}
这个例子中, 申明了一个 printerQueue
, 然后往里添加了6个任务Operation, 每个Operation输出一个字后sleep 3秒. 执行结果如下:
--- Example of: OperationQueue ---
厉
害
了
我
的
哥
[ spent: 3.00513 ] seconds
可以看出, 每个Operation都要花3秒, 而printerQueue
实际也只花费了3秒, 说明这些Operation都是并行执行的.
输出的顺序虽然和Operation加入的顺序是一样的,这其实是一种巧合, 实际情况不一定是这样的. 如果你将maxConcurrentOperationCount
设置为2(取消上面的注释行),
printerQueue.maxConcurrentOperationCount = 2
你会发现输出是这样的:
--- Example of: OperationQueue ---
厉
害
我
了
哥
的
[ spent: 9.01079 ] seconds
每个Operation花费3秒, 最大并发数为2, 两两一组可分为3组, 6个Operation总共花费了3*3 = 9秒, 输出顺序和加入Operation的顺序不完全一样,进一步说明加入OperationQueue的Operation是concurrent执行的.
文中的示例代码均可参见我的GitHub: https://github.com/zhihuitang/GCDExample