多线程
多线程技术是iOS开发里十分常见的,下面会介绍GCD的常用多线程技术。首先简单了解一下几个概念:
- 同步,异步:任务执行时是否开多线程,同步为单线程,异步为多线程
- 串行,并发:任务执行的方式,串行为顺序执行,并发为同时执行
- 队列:任务存放的地方,线程从队列里取出任务执行。分为串行队列和并发队列
- 线程:执行任务的实际运作单位,主线程从主队列里取任务,更新UI必须在主线程完成
GCD:
GCD是操作最简单,也是我们最常用的多线程技术。在OC里是一套C语言API,在Swift里对其封装进一步简化,本文会使用Swift语法。
一。主队列,全局队列
先来看看这个GCD的最常用公式:
DispatchQueue.global().async {
//long time operation
DispatchQueue.main.async {
//update UI
}
}
它分为以下几个步骤:
- 通过
DispatchQueue.global()
获取全局队列 - 在全局队列中
async
并发执行耗时操作,如下载 - 通过
DispatchQueue.main
获取全局队列 - 在全局队列中
async
并发执行更新UI的动作
那么为什么要如此使用呢?
- 首先,更新UI的操作必须放在主线程中,否则会报错。这里
DispatchQueue.main
是获取主队列,它是一个串行队列,而主线程要执行的任务就是到这个队列里去取的。 -
DispatchQueue.global()
是获取系统提供的全局队列,该队列是并发队列。加入该队列的任务会开辟多条线程共同执行,就好像一个页面会下载显示很多张图片,用全局队列的话图片会一张张显示出来,而如果用主队列的话,所有图片下载完成才统一显示。 - 这里不管是主队列还是全局队列都使用
async
,而没有使用sync
是因为sync
同步操作,相当于将所有操作都由主线程来执行,这会阻塞主线程,也就是UI上的卡顿。
这里写了一个小实验比较上面的各种情况。(因为图片地址在公司服务器上,这里不公布代码,只看结果)。
点击一个button,会Push到下一个页面,而下一个页面中会有一个CollectionView,共包含10个CollectionViewCell,每一个cell中都会下载并显示一张图片。
-
第一张图用的是之前的标准公式,点击按钮后立即Push到下一个页面,并且图片也相继显示出来。
-
第二张图是将
DispatchQueue.global()
换成了DispatchQueue.main()
。可以看到,点击按钮后立即Push到下一个页面,所有图片下载完成后再一起显示,这是因为主队列是串行队列,所有的更新UI任务都排在了下载任务之后,因此要等所有的图片下载完成后,UI才会开始显示。
-
第三张图是将
DispatchQueue.global().async
换成了DispatchQueue.global().sync
。可以看到,点击按钮后卡顿了一会儿,再进入下一个页面。这是因为sync
是同步操作,会阻塞主线程。
由以上三张图可看出,常用公式是最合适的选择
二。创建队列
let queue = DispatchQueue(label: "com.demo.queue", qos: .background, attributes: .concurrent)
- qos是指队列的优先级,从background到userinteractive,优先级从低到高
- concurrent指定了该队列为并发队列,若不穿,则默认为串行队列
自建的队列最终会被归入到主队列和全局队列中去,那么为什么还要创建它们呢,也是便于对一系列任务的管理。
三。派发组DispatchGroup
开发中也许会有这一种情况,我们要同时请求好几个接口后,再刷新UI。这种对多个异步请求的管理,用DispatchGroup最合适。
用法如下
let group = DispatchGroup()
group.enter()
//task1
group.leave()
group.enter()
//task2
group.leave()
group.enter()
//task3
group.leave()
group.notify(queue: DispatchQueue.main) {
//update UI
print("更新UI")
}
更新UI的操作会在task1,task2,task3这3个异步任务完成后再执行。
四。信号量semaphore
简单的说,在异步操作中,任务完成的顺序是不确定的。semaphore可以使得我们将异步操作按顺序同步完成。
这里有一片博客,对其进行了详细的介绍
五。屏障barrier
想象有这样一个操作。从数据库里执行两次读的任务read1和read2,并发执行并没有什么问题。可如果要在read1和read2中间加入一个write1,要求read1读取的是write之前的数据,read2读取的是write之后的数据,那么应该如何处理呢?
首先,如果write不能用普通的并发操作。因为并发队列的特性是无法确保read1,read2以及write的执行顺序,这可能会发生read2读取的是write之前的数据。
这里我们需要用到barrier。顾名思义,它是作为一个屏障,隔开了read1和read2,在这个屏障内的进行write,也会保证,read1->write->read2这样的一个执行顺序
DispatchQueue.global().async {
//read1
}
DispatchQueue.global().async(flags: .barrier) {
//write
}
DispatchQueue.global().async {
//read2
}
六。延迟执行
这里用GCD封装一个延迟执行
func delay(_ timeInterval: TimeInterval, closure: @escaping ()->()) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + timeInterval, execute: closure)
}
调用起来非常简单和明了
delay(3) {
//task
}
值的一体的是,该延迟执行指的并不是在3秒后执行task,而指的是,在3秒后,将task加入主队列。所以当主队列有大量处理或者本身已经延迟的情况下,真正执行的时间会比3秒更长。不过一般来说,在主队列非阻塞的情况下,该方法还是算非常简洁有效的。
七。创建单例
得益于GCD中的dispatch_once
,这个函数会让程序只执行一次,我们在OC中用它来实现单例。
static dispatch_once_t pred;
dispatch_once(pred, ^{
//init
});
不过为了防止单例类通过alloc或者new的方法实例化,实际上要写的代码还有很多,相比较下来Swift的单例实现就要简单很多。
class Singleton {
static let shared = Singleton()
private init() {}
}
八。死锁
死锁一般是指同步任务的相互等待,造成程序崩溃。
例如,我们在ViewController的ViewDidLoad
里加入这一句,程序执行到这里立刻就会崩溃。
DispatchQueue.main.sync {}
因为ViewDidLoad是从主线程执行,而DispatchQueue.main.sync {}
也是在主线程里同步执行。ViewDidLoad需要等待DispatchQueue.main.sync {}完成,但ViewDidLoad又是先加入主队列的,因此DispatchQueue.main.sync {}要执行必须先等ViewDidLoad执行完毕。因此他们相互等待,造成死锁。