上篇文章讲了 NSThread 的基本使用,本文讨论一下 GCD 的基本使用
GCD 简介
GCD(全称 Grand Central Dispatch),苹果在 iOS 4中首次推出,为多核的并行运算提出的解决方案,GCD 会自动管理线程的生命周期、创建线程、调度任务、销毁线程,开发者只需要关心要执行的任务而不用考虑任何线程管理的相关代码。由于 GCD 是基于 C 的 API,在 OC 和 Swift 2 都是 C 语言风格,在 Swift 3 中苹果使用了全新的 Swift 语法风格改写了 GCD,本文会展示 OC 和 Swift 3 两种语言的写法使用 GCD
任务和队列
使用 GCD 之前先了解一些基本概念
任务
任务就是执行操作的意思,就是你在线程中执行的操作,GCD 中任务的代码放在 Block(DispatchWorkItem)中,执行任务有两种方式:同步执行(sync) 和 异步执行(async),他们的区别就是 是否会开启新的线程
- 同步执行(sync),只会在当前线程执行任务,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行
- 异步执行(async),它会开启新的线程去执行 Block 中的任务,当前线程会继续执行,不会阻塞当前线程
队列
队列这里是指任务队列,队列是一种特殊的线性表,队列遵循 FIFO 模式(先进先出)原则,意思就是先进队列的任务会先被执行,后来的任务会被插入队尾(就和排队一样买票一样,排在第一个的先买,排在最后的最后买),GCD 中有两种队列:串行队列 和 并发队列
- 串行队列(Serial Dispatch Queue),GCD 会按照先进先出的原则,取出任务,按顺序执行
- 并发队列(Concurrent Dispatch Queue),可以让多个任务并发(同时)执行,GCD会按照先进先出的原则把任务取出,但是它会开启新线程去执行取出的任务,直到取完所有任务。由于取出任务很快,看起来就像是同时取出来一样,GCD 会根据系统的资源开启合适数量的线程用来并发执行任务
GCD 的核心就是:将任务添加到队列,另外,所有的 Dispatch Queue 自身都是线程安全的,可以在多个线程并行访问
GCD 的基本使用
GCD 的使用步骤:先创建一个串行或者并发的队列( Dispatch Queue ),然后将需要执行的任务添加到队列中,系统就会自动执行队列中任务
OC 中创建 Dispatch Queue
|
|
使用 dispatch_queue_create()
方法创建一个队列,需要传入两个参数,第一个参数是表示队列的唯一标识符,第二个参数表示队列的类型,DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并发队列,对于并发队列,还可以使用 dispatch_get_global_queue()
来创建全局并发队列
|
|
全局并发队列是 GCD 为我们创建的一个并发队列,dispatch_get_global_queue()
,需要传入两个参数,第一个参数是表示队列的优先级,有DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
和 DISPATCH_QUEUE_PRIORITY_BACKGROUND
四种,一般使用DISPATCH_QUEUE_PRIORITY_DEFAULT
,第二个参数是苹果为以后设计的,暂时用不到,传 0
即可,注意,全局并发队列系统本身也会使用到
系统还创建了一个主队列(对应主线程,即UI线程),主线程的作用是处理UI事件(点击事件、滚动事件、拖拽事情),可以通过如下方式获取
|
|
Swift 中创建 Dispatch Queue
在 Swift 中,最简单的可以使用以下方法创建一个队列
|
|
这样初始化的队列是一个默认配置的队列,也可以显式的指明属性来创建一个队列
|
|
分析一下几个参数的作用:
- label:队列的标识符,方便调试
- qos:队列的 quality of service,用来指明队列的优先级,后文会讲到
- attributes:队列的属性,是一个结构体,包括两个值:initiallyInactive(表示队列需要手动触发) 和 concurrent(表示并发队列)
- autoreleaseFrequency:自动释放频率,有些队列是会在执行完任务后自动释放的,有些比如Timer等是不会自动释放的,需要手动释放
所以在 Swift 上可以这样创建创建一个串行/并发队列
|
|
主队列和全局队列可以这样获取
|
|
QoS
QoS 全称 quality of service,它是一个结构体,用来规定队列的优先级,Qos 有以下四种类型:从上到下优先级依次降低
- User Interactive:和用户交互相关,比如动画、用户连续拖拽的计算等等,优先级最高
- User Initiated:次高优先级,需要马上得到结果,比如push一个ViewController之前的数据计算
- Default:默认优先级
- Utility:普通优先级,可以执行很长时间再通知用户结果,比如下载一个文件,给用户下载进度
- Background:用户不可见,最低优先级,比如在后台存储大量数据
把任务添加到队列
在 GCD 中队列有:串行队列、并发队列、全局队列和主队列
执行任务的方式有:同步执行和异步执行
串行队列 + 同步执行
OC
|
|
Swift
|
|
打印结果如下:
|
|
从执行结果可以看出所有任务都在当前线程(主线程)执行,而由于队列是串行队列,所以任务是按顺序执行的,而且所有任务都是在 start
和 end
之间打印的,说明任务是添加的队列中立即执行的
串行队列 + 异步执行
OC
|
|
Swift
|
|
打印结果如下:
|
|
从执行结果可以看出系统开启一条新线程,而由于队列还是串行队列,所以任务是按顺序执行的,而且所有任务都是在 start
和 end
之后打印的,说明任务添加到队列并没有立即执行,而是所有任务添加完成才开始执行
并发队列 + 异步执行
OC
|
|
Swift
|
|
打印结果如下:
|
|
从执行结果可以看出系统开启了3条新线程,由于队列是并行行队列,所以任务是并行的、顺序不定,而且所有任务都是在 start
和 end
之后打印的,说明任务是添加到队列并没有立即执行,而是所有任务添加完成才开始异步执行的
并发队列 + 同步执行
OC
|
|
Swift
|
|
打印结果如下:
|
|
从执行结果可以看出任务全部在主线程中执行,由于只有一条线程,所以任务是按顺序执行,而且所有任务都是在 start
和 end
之间打印的,说明任务是添加到队列中是立即执行的
主队列 + 异步执行
OC
|
|
Swift
|
|
打印结果如下:
|
|
从执行结果可以看出任务全部在主线程中执行,并没有开启新线程,由于只有一条线程,所以任务是按顺序执行,而且所有任务都是在 start
和 end
之后打印的,说明任务是添加到队列中不是立即执行的,而是将所有任务添加到队列之后才开始同步执行的
主队列 + 同步执行
OC
|
|
Swift
|
|
打印结果如下:
|
|
打印完 start
之后,系统卡死了,后面的代码不执行了,原因是:循环等待,造成线程死锁
我们在主线程执行 mainQueueSync()
,又把下载图片的任务放到了主队列里同步执行,同步执行的特点就是立即执行任务,当我们把第一张图片下载放到主队列的时候,它会立即执行,但是现在主线程正在出处理 mainQueueSync()
,所以下载图片任务必须等 mainQueueSync()
执行完,而mainQueueSync()
执行到下载第一张图片下载完之后,才能继续下载第二张和第三张图片的任务,于是 mainQueueSync()
和 下载第一张图片的任务都在互相等待对方执行完毕,于是线程就卡死了,什么任务都执行不了了,后面的 end
自然也不会打印了,如果在非主线程执行 mainQueueSync()
,则不会出现死锁问题,任务会按顺序在主队列执行
由于全局队列本质就是 并发队列,它的执行效果和并发队列是一致的
总结一下各种队列+任务:
并发队列 | 串行队列 | 主队列 | |
---|---|---|---|
同步(sync) | 不开启新线程,串行执行任务 | 不开启新线程,串行执行任务 | 不开启新线程,串行执行任务 |
异步(async) | 开启多个线程,并发执行任务 | 有开启一条新线程,串行执行任务 | 不开启新线程,串行执行任务 |
GCD 线程之间的通讯
开发过程中,我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作,当我们有时候在其他线程完成了耗时操作时,需要回到主线程进行UI刷新等操作,这就用到了线程之间的通讯
|
|
GCD 的其他用法
DispatchGroup
DispatchGroup 可以用来管理一组任务的执行,并且监听任务都完成的事件,DispatchGroup 中的任务可以是在不同的队列中,使用DispatchGroup 可以用来同步代码,比如:多个异步网络请求同时发出,等待所有网络请求都完成后刷新 UI,看一下如何使用 DispatchGroup
OC
|
|
Swift
|
|
打印结果如下:
|
|
队列组关联的任务全部完成之后,会发出同步信号,在 notify
方法中监听到任务全部完成并执行 notify
中的代码,另外,还可以使用 DispatchGroup 的 enter()
和 leave()
手动把任务加入队列组,enter()
表示接下来的任务块加入 DispatchGroup ,leave()
表示任务块代码结束, enter()
和 leave()
必须成对出现,通过这种方式可以让就可以让 AFN 的网络请求添加的到 DispatchGroup 中,达到多个网络请求同步,上面的代码也可以这样写:
OC
|
|
Swift
|
|
DispatchGroup 中还有一个 wait()
方法,可以设置等待时间,在等待时间内会一直等待任务组中的任务完成,当任务完成或者等待超过等待时间会结束等待,wait()
会阻塞当前线程一直等待(所以不要放在主线程调用)
OC
|
|
Swift
|
|
这里再说一下 DispatchWallTime
和 DispatchTime
的区别,当计算机进入睡眠状态的时候, DispatchTime
也会进入睡眠状态,而 DispatchWallTime
会一直运行,不会睡眠,这两个的区别使用应该是用于 macOS 开发
GCD 延迟执行
有时候程序需要对代码块里面的任务项进行延时操作,GCD 可以通过调用一个方法来指定某个任务在延迟特定的时间后再执行
OC
|
|
Swift
|
|
GCD 一次性代码
在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就可以使用 GCD 的 dispatch_once
方法,使用 dispatch_once
能保证某段代码在程序运行过程中只被执行1次
OC
|
|
dispatch_once
在 Swift 3 中已经被废弃了,实际上 Swift 内部已经在使用 dispatch_once
机制支持线程安全的懒加载初始化和静态变量了,Swift 的语言特性决定了我们不需要在 Swift 里面使用 dispatch_once
所以苹果就显式的取消了,如果需要在 Swfit3 中实现原来 dispatch_once
机制可以参考下面两篇文章
GCD 快速迭代
通常我们使用 For 或者 While 循环遍历,但是普通的循环是在主线程中执行,如果循环多次过多可能会影响主线程执行,导致 UI 界面卡顿,影响用户体验,GCD 提供了一个快速循环遍历的方法,会开启新线程执行任务,但是迭代执行的顺序是不固定的,比如:从目录里遍历文件就可以使用这个方法
OC
|
|
Swift
|
|
DispatchSemaphore
DispatchSemaphore 是传统计数信号量的封装,用来控制资源被多任务访问的情况
有三个主要的相关方法:dispatch_semaphore_create
,dispatch_semaphore_signal
,dispatch_semaphore_wait
分别介绍一下:
dispatch_semaphore_create
:传入一个信号量的初始值来创建一个信号量,传入的初始值必须大于等于0dispatch_semaphore_wait
:这个方法的判断信号量的值大于0,则该方法所处的线程就会继续执行下面的语句,并且将信号量的值减1,该方法还可以传入一个超时时间,如果在等待期间获取不到信号量或者信号量一直为0,那么超时时间之后会自动执行后面的代码dispatch_semaphore_signal
:这个方法会把信号量的值加1
关于信号量借别人一个停车场的例子:停车场剩余4个车位,那么即使同时来了四辆车也能停的下,如果此时来了五辆车,那么就有一辆需要等待。 信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait
函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)
), 调用一次 dispatch_semaphore_signal
,剩余的车位就增加一个,调用一次dispatch_semaphore_wait
剩余车位就减少一个,当剩余车位为 0 时,再来车(即调用dispatch_semaphore_wait
)就只能等待,有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车,而有些车主就想把车停在这,所以就一直等下去
下面看一下如何使用信号量:
OC
|
|
Swfit
|
|
以下是打印结果:
|
|
当信号量初始值为2时,打印结果如下:
|
|
当信号量初始值为3时,打印结果如下:
|
|
从上面的打印结果可以看出,通过信号量可以控制并发的数量,并且可以阻塞线程达到线程同步的目的,也可以利用信号量保证只有一个线程访问同一份资源
DispatchWorkItem
DispatchWorkItem 代替之前的 dispatch_block_t
, 它是一个代码块,它可以在任意一个队列上被调用,因此它里面的代码可以在后台运行,也可以在主线程运行,在 DispatchQueue 执行操作除了直接传了一个 () -> Void
类型的闭包外,还可以传入一个 DispatchWorkItem,看一下如何使用
|
|
DispatchWorkItem 的初始化函数 DispatchWorkItem(qos: .default, flags: .enforceQoS)
可以设置两个参数
- QoS:quality of service 上面已经解释过了
- DispatchWorkItemFlags:指定这个任务的配饰信息,这个参数分为两组
- 执行情况:barrier、detached、assignCurrentContext
- QoS覆盖信息:noQoS(没有QoS)、inheritQoS(继承Queue的QoS)、enforceQoS(自己的QoS覆盖Queue)
GCD 还有很多高级用法需要深入的学习,本文仅仅列举了 GCD 的一些基本用法,不过已经可以满足日常的大部分多线程的开发工作了
其他相关文章:
本文参考:
Grand Central Dispatch (GCD) and Dispatch Queues in Swift 3
本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!