GCD Cheat Sheet

基于自 raywenderlich.com 在2015年的两篇文章 Grand Central Dispatch Tutorial for Swift 3: Part 1/2Part 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 版本的头文件里看对应的文档,非常详细。

基本概念

  1. Serial vs. Concurrent
    这两个词用来描述执行多个任务时任务之间的关系:Serial,常译作「串行」,表示这些任务同时最多只能有一个任务在执行;Concurrent,在这种语境下常译作「并行」,表示这些任务有可能同时执行多个。
  2. Synchronous vs. Asynchronous
    这两个词用来描述函数返回的时机以及函数的运作方式:Synchronous 常译作「同步」,表示函数占用当前线程直到运行结束才返回结果;Asynchronous 常译作「异步」,表示函数立即返回结果,而把实际的任务放在其他线程里运行。
  3. Concurrency vs. Parallelism
    两者的区别在于,前者需要进行上下文切换造成同时执行两个或多个线程的假象。Parallelism 在多核设备上才能进行,而得益于多核,Concurrency 也可以采用后者一样的方式,这取决于系统。


    Concurrency vs. Parallelism
  4. GCD and Queue
    GCD 全称 Grand Central Dispatch, 是 libdispatch 这个库的外部代号,它提供 dispatch queues 来执行任务;dispatch queue 都是线程安全的,并且保证加入的任务按照 FIFO 规则来运行;dispatch queue 无法保证任务的精确执行时间,需要控制执行时间就直接使用 NSThread;dispatch queue 分为 serial queue 及 concurrent queue 两类,与第1点的概念匹配。
  5. Serial Queues vs. Concurrent Queues
    serial queues 保证一次只执行一个任务,当前任务结束后才能执行下一个任务,但不保证两个任务之间的间隔时间;concurrent queue 唯一能保证的是加入的任务的执行顺序是按照它们加入的时间来的。
Serial Queue

Concurrent Queue

Dispatch Queue 优选

  1. 预定义 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

关于 QOS 的具体解释,可以查询这篇文档 Prioritize Work with Quality of Service Classes

更新 UI 切记一定要在 main queue 里进行,不然很有可能跟你的预期不一样。想我还是个大菜鸟的时候,就犯了这个错误,死活找不到原因。在 Xcode 9 里有了 Main Thread Checker 可以检测到不在主线程更新 UI 的代码,貌似默认是开启的,在 Edit Scheme -> Run -> Disgnostics 里。

  1. 自定义 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

在 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(),尽管这两个是等价的。

常规使用

  1. 任务封装 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

  1. 在 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 执行时是唯一执行的任务,如下图所示。


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 的作用。

其它

  1. 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)
  1. Dispatch Source
    Dispatch Source 用来监视一些系统底层事件并自动做出反应:在 dispatch queue 中提交 Block 对事件作出处理,感觉很熟悉是吧。我还没处理底层的经验,文章使用的例子是利用 dispatch source 对应用恢复运行状态做出反应,然而还是不懂这个的用处。作者表示为了在现实中能派上用场利用 dispatch source 实现了一个 stack trace tool 用于调试,然而,我看不懂,觉得总结不出个啥来。

  2. 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

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

推荐阅读更多精彩内容