锐眼洞察 | 详谈 GCD(Grand Central Dispatch)的基本使用

作者:TalkingData研发工程师 张永超

本文为TalkingData原创,未经授权禁止转载。申请授权请在评论中留言联系!

1. GCD(Grand Central Dispatch)介绍

Grand Central Dispatch (GCD) 是 Apple 开发的一种多核编程的解决方法。该方法在 Mac OS X 10.6 Lion 中首次推出,并随后被引入到了 iOS4.0 中。GCD 是一个替代诸如 NSThread, NSOperationQueue, NSInvocationOperation 等技术的更高效和强大的技术。

GCD 属于系统级的线程管理技术,在 Dispatch queue 中执行相关任务的性能非常高。GCD 的源代码已经开源,感兴趣的可以参考Grand Central Dispatch。 GCD 中的FIFO队列称为 dispatch queue,以用来保证先进入队列的任务先得到执行。

GCD 简述

  • 和Operation queue相同,都是基于队列的并发编程API,均是通过集中管理、协同使用的线程池。

GCD具有5个不同的队列:

  1. 运行在主线程中的 Main queue
  2. 三个不同优先级的队列(High Priority Queue,Default Priority Queue,Low Priority Queue)。
  3. 更低优先级的后台队列 Background Priority Queue,主要用于I/O。
  4. 用户可自定义创建队列:串行或并行队列
  5. 具体的操作时在多线程上还是单线程上,主要依据队列的类型和执行方法,并行队列异步执行在多线程,并行队列同步执行只会在单线程(主线程)执行。

GCD 基本的概念

标准队列

标准队列是指GCD预定义的队列,在iOS系统中主要有两个:

WX20180125-183854

自定义队列

用户可以自定构建队列,并设置队列是并行还是串行:

WX20180125-183941

同步\异步线程创建

用户也可以根据需要自行构建同步\异步线程:

WX20180125-184016

2. 队列(dispatch queue)

  • Serial(串行队列):又叫做 private dispatch queues,同时只执行一个任务(Task)。Serial queue 常用语同步访问特定的资源或数据,当创建了多个 Serial queue 时,虽然各自是同步的,但是 Serial queue 之间是并发执行的。
  • Main dispatch queue: 全局可用的 Serial queue,在应用程序的主线程上执行。
  • Concurrent(并行队列):又叫做 global dispatch queues,可以并发的执行多个任务,但是执行的顺序是随机的。iOS 系统提供了四个全局并发队列,这四个队列有着对应的优先级,用户是不能创建全局队列的,只能获取,如下:

WX20180125-184318

  • Custom(自定义队列):用户可以自定义队列,使用 dispatch_queue_create 函数,并附带队列名和队列类型参数,其中队列类型默认是NULL,代表DISPATCH_QUEUE_SERIAL串行队列,可以使用参数DISPATCH_QUEUE_CONCURRENT设置并行队列。

 

WX20180125-184351
  • 自定义队列的优先级:在自定义队列的时候,可以设置队列的优先级,使用dipatch_queue_attr_make_with_qos_class或者dispatch_set_target_queue方法来设置,如下:

 

WX20180125-184955
  • dispatch_set_target_queue:此方法不仅能够设置队列优先级,还可以设置队列的层级体系,比如让多个串行队列和并行队列在统一的一个串行队列里执行,如下:

WX20180125-185418

队列的类型

在iOS中,队列本身默认是串行的,只能执行一个单独的block,但是队列也可以是并行的,同一时间执行多个block。

在创建队列时,我们通常使用dispatch_queue_create函数:

WX20180125-185443

iOS中的公开5个队列:主队列(main queue)、四个通用调度队列以及用户定制的队列。对于四个通用调度队列,分别为:

  • QOS_CLASS_USER_INTERACTIVE:user interactive 等级表示任务需要被立即执行已提供最好的用户体验,更新UI或者相应事件等,这个等级最好越小规模越好。
  • QOS_CLASS_USER_INITIATED:user initiated等级表示任务由UI发起异步执行。适用场景是需要及时结果同时又可以继续交互的时候。
  • QOS_CLASS_UTILITY:utility等级表示需要长时间运行的任务,伴有用户可见进度指示器。经常会用来做计算,I/O,网络,持续的数据填充等任务。
  • QOS_CLASS_BACKGROUND:background等级表示用户不会察觉的任务,使用它来处理预加载,或者不需要用户交互和对时间不敏感的任务。

一个典型的实例就是在后台加载图片:

WX20180125-190159

队列类型的使用

那么具体在操作中,什么时候使用什么类型的队列呢?通常有如下的规则:

  • 主队列:主队列通常是其他队列中有任务完成,需要更新UI时,例如使用延后执行dispatch_after的场景。
  • 并发队列:并发队列通常用来执行和UI无关的后台任务,但是有时还需要保持数据或者读写的同步,会使用dispatch_sync或者dispatch_barrier_sync 同步。
  • 自定义顺序队列:顺序执行后台任务并追踪它时。这样做同时只有一个任务在执行可以防止资源竞争。通常会使用dipatch barriers解决读写锁问题,或者使用dispatch groups解决锁问题。

可以使用下面的方法简化QoS等级参数的写法:

WX20180125-190306

dispatch_once用法

dispatch_once_t要是全局或static变量,保证dispatch_once_t只有一份实例。

WX20180125-190343

dispatch_async

设计一个异步的API时调用dispatch_async(),这个调用放在API的方法或函数中做。让API的使用者有一个回调处理队列。

WX20180125-190406
可以避免界面会被一些耗时的操作卡死,比如读取网络数据,大数据IO,还有大量数据的数据库读写,这时需要在另一个线程中处理,然后通知主线程更新界面,GCD使用起来比NSThread和NSOperation方法要简单方便。

WX20180125-193828

dispatch_after延后执行

dispatch_after只是延时提交block,不是延时立刻执行。

WX20180125-190811

范例,实现一个推迟出现弹出框提示,比如说提示用户评价等功能。

WX20180125-190834

例子中的dispatch time的参数,可以先看看函数原型


WX20180125-190857
第一个参数为DISPATCH_TIME_NOW表示当前。第二个参数的delta表示纳秒,一秒对应的纳秒为1000000000,系统提供了一些宏来简化:

WX20180125-190923
如果要表示一秒就可以这样写:

WX20180125-190949
dispatch_barrier_async使用Barrier Task方法Dispatch Barrier解决多线程并发读写同一个资源发生死锁

Dispatch Barrier确保提交的闭包是指定队列中在特定时段唯一在执行的一个。在所有先于Dispatch Barrier的任务都完成的情况下这个闭包才开始执行。轮到这个闭包时barrier会执行这个闭包并且确保队列在此过程不会执行其它任务。闭包完成后队列恢复。需要注意dispatch_barrier_async只在自己创建的队列上有这种作用,在全局并发队列和串行队列上,效果和dispatch_sync一样。

WX20180125-191022WX20180125-191039

Swift示例:

WX20180125-191105

都用异步处理避免死锁,异步的缺点在于调试不方便,但是比起同步容易产生死锁这个副作用还算小的。

dispatch_apply进行快速迭代

类似for循环,但是在并发队列的情况下dispatch_apply会并发执行block任务。

WX20180125-191137

dispatch_apply能避免线程爆炸,因为GCD会管理并发。

WX20180125-191203
示例:

WX20180125-191228
Block组合Dispatch_groups

dispatch groups是专门用来监视多个异步任务。dispatch_group_t实例用来追踪不同队列中的不同任务。

当group里所有事件都完成GCD API有两种方式发送通知,第一种是dispatch_group_wait,会阻塞当前进程,等所有任务都完成或等待超时。第二种方法是使用dispatch_group_notify,异步执行闭包,不会阻塞。

第一种使用dispatch_group_wait的swift的例子:

WX20180125-191306WX20180125-191324
OC例子:

WX20180125-191355

第二种使用dispatch_group_notify的swift的例子:

WX20180125-191426

OC例子:

WX20180125-191524

如何对现有API使用dispatch_group_t:

WX20180125-191614

注意事项

  • dispatch_group_async等价于dispatch_group_enter() 和 dispatch_group_leave()的组合。
  • dispatch_group_enter() 必须运行在 dispatch_group_leave() 之前。
  • dispatch_group_enter() 和 dispatch_group_leave() 需要成对出现的

3. Dispatch Block

队列执行任务都是block的方式

创建block。

WX20180125-191921

dispatch_block_wait:可以根据dispatch block来设置等待时间,参数DISPATCH_TIME_FOREVER会一直等待block结束。

WX20180125-192121

dispatch_block_notify:可以监视指定dispatch block结束,然后再加入一个block到队列中。三个参数分别为,第一个是需要监视的block,第二个参数是需要提交执行的队列,第三个是待加入到队列中的block。

WX20180125-192344

dispatch_block_cancel:iOS8后GCD支持对dispatch block的取消​

WX20180125-192537

使用dispatch block object(调度块)在任务执行前进行取消

dispatch block object可以为队列中的对象设置。

示例,下载图片中途进行取消:

WX20180125-192612WX20180125-192632

Dispatch IO 文件操作

dispatch io读取文件的方式类似于下面的方式,多个线程去读取文件的切片数据,对于大的数据文件这样会比单线程要快很多。

WX20180125-192655
  • dispatch_io_create:创建dispatch io
  • dispatch_io_set_low_water:指定切割文件大小
  • dispatch_io_read:读取切割的文件然后合并。

苹果系统日志API里用到了这个技术,可以在这里查看。

WX20180125-192731WX20180125-192816

此内容就到这里了。其他的技术点将会陆续放出,GCD是个好东西,但是在使用的时候一定要理清楚其含义,否则很容易出现不可调试的问题等。

发表评论

电子邮件地址不会被公开。 必填项已用*标注