GCD和Operation/OperationQueue 看这一篇文章就够了

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
15235190270597.jpg

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的可实例化子类BlockOperationInvocationOperation, 这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: <NSThread: 0x604000074180>{number = 11, name = (null)}
6 in blockOperation: <NSThread: 0x60c000072d80>{number = 1, name = main}
4 in blockOperation: <NSThread: 0x608000079e40>{number = 7, name = (null)}
3 in blockOperation: <NSThread: 0x604000073e00>{number = 8, name = (null)}
2 in blockOperation: <NSThread: 0x60c0000774c0>{number = 9, name = (null)}
5 in blockOperation: <NSThread: 0x600000078ec0>{number = 6, name = (null)}
1 in blockOperation: <NSThread: 0x60800007af40>{number = 10, name = (null)}
7 in blockOperation: <NSThread: 0x604000072b80>{number = 5, name = (null)}
10 in blockOperation: <NSThread: 0x60c000072d80>{number = 1, name = main}
9 in blockOperation: <NSThread: 0x604000074180>{number = 11, name = (null)}
All block operation task finished: <NSThread: 0x604000072b80>{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可以查看处理完成的图片:

15235348249665.jpg

从日志输出可以看出, 总的图片处理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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容