iOS复习-GCD
简介
GCD,全名Grand Central Dispatch,是基于C语言的一套多线程开发API,一听名字就是个狠角色,也是目前苹果官方推荐的多线程开发方式。 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。可以说是使用方便,又不失逼格。
GCD 任务和队列
学习 GCD 之前,先来了解 GCD 中两个核心概念:[任务]和[队列]。
【任务】:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:【同步执行(sync)和异步执行(async)】。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
- 同步执行(sync):
- 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
- 只能在当前线程中执行任务,不具备开启新线程的能力。
- 异步执行(async):
- 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
- 可以在新的线程中执行任务,具备开启新线程的能力。
【队列】也只有两种:串行队列(Serial Dispatch Queue)、并发队列(Concurrent Dispatch Queue),队列就是在存放任务也就是线程里的操作,创建了一个队列,也就是开启了一个线程的容器,不是在DispatchQueue.main队列中的线程,都是子线程。
-
串行队列(Serial Dispatch Queue):
让任务一个接着一个有序的执行,一个任务执行完毕后,再执行下一个任务。 -
并行队列(Concurrent Dispatch Queue):
可以让多个任务同时执行,自动开启多个线程同时执行多个任务。但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的,实际上,一个CPU,在一个时间点,只能开启一个线程,并行是说CPU在多个线程之间快速来回切换,我们看上去像多个线程一起在执行。
使用GCD就只有两步:创建任务,把任务放进Queue里。
GCD.png基本使用
- 全局异步队列
DispatchQueue.global().async {
//耗时操作
}
- 主队列
DispatchQueue.main.async {
//刷新UI
}
3.默认队列(是串行队列)
let queue = DispatchQueue(label: "com.Leo.demoQueue")
- 显式的设置队列
let qos = DispatchQoS.default
let attributes = DispatchQueue.Attributes.concurrent
let autoreleaseFrequency = DispatchQueue.AutoreleaseFrequency.never
let queue = DispatchQueue(label: label, qos: qos, attributes: attributes, autoreleaseFrequency: autoreleaseFrequency, target: nil)
-
label
队列的标识符,方便调试 -
qos
队列的quality of service。用来指明队列的“重要性”,后文会详细讲到。 -
attributes
队列的属性。类型是DispatchQueue.Attributes
,是一个结构体,遵循了协议OptionSet。是串行/并行队列。let attributes = DispatchQueue.Attributes.concurrent
表示并行队列 -
autoreleaseFrequency
。顾名思义,自动释放频率。有些队列是会在执行完任务后自动释放的,有些比如Timer等是不会自动释放的,是需要手动释放。
- 延迟操作
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
// 2秒后执行
}
- 队列组
队列组就是把任务放在DispatchGroup中(入组),当任务执行完毕时(出组),即当DispatchGroup中没有任务时,调用监听方法notify,注意:入组和出组一定要成对出现,有几个入组,就一定需要有几个出组。只有当每个group.enter()与group.leave()都配对完成后,才会进入到group.notify()执行后续。
注意: group不是对加入到其中的线程进行顺序执行,而是对所加入的这些线程全部都执行完毕后,在group.notify的通知中进行数据的排序等相关操作。
let group = DispatchGroup()
// A任务入组
group.enter()
// A任务异步操作
DispatchQueue.global().async(group: group, execute: DispatchWorkItem(block: {
sleep(1)
print("task A ...")
// 出组
group.leave()
}))
// B任务入组
group.enter()
// B任务异步操作
DispatchQueue.global().async(group: group, execute: DispatchWorkItem(block: {
sleep(2)
print(" task B ...")
// 出组
group.leave()
}))
// 主线程监听,只有当队列组中没有任务,才会执行闭包。如果多次调用该方法,每次都会去检查队列组中是否有任务,如果没有任务才执行
group.notify(queue: DispatchQueue.main) {
print("complete...")
}
死锁
- 一个🌰: 2861521449224_.pic.jpg
- 分析一下为什么会死锁:
我们先做一个定义:- (void)viewDidLoad{} ---> 任务A,GCD同步函数 --->任务B。
总而言之呢,大概是这样的,首先,任务A在主队列,并且已经开始执行,在主线程打印出====A=====%@,然后这时任务B被加入到主队列中,并且同步执行,这尼玛事就大了,系统说,同步执行啊,那我不开新的线程了,任务B说我要等我里面的Block函数执行完成,要不我就不返回,但是主队列说了,玩蛋去,我是串行的,你得等A执行完才能轮到你,不能坏了规矩,同时,任务B作为任务A的内部函数,必须等任务B执行完函数返回才能执行下一个任务。那就造成了,任务A等待任务B完成才能继续执行,但作为串行队列的主队列又不能让任务B在任务A未完成之前开始执行,所以任务A等着任务B完成,任务B等着任务A完成,等待,永久的等待。所以就死锁了。
还有一种死锁,简单的代码如下
queueA.sync {
queueB.sync {
queueC.sync {
queueA.sync {
}
}
}
}
死锁的原因很简单,形成了一个相互阻塞的环。
知识点
-
对于单核CPU来说,不存在真正意义上的并行,所以,多线程执行任务,其实也只是一个人在干活,CPU的调度决定了非等待任务的执行速率,同时对于非等待任务,多线程并没有真正意义提高效率。
-
线程可以简单的认为就是一段代码+运行时数据。
-
同步执行会在当前线程执行任务,不具备开辟线程的能力或者说没有必要开辟新的线程。并且,同步执行必须等到Block函数执行完毕,dispatch函数才会返回,从而阻塞同一串行队列中外部方法的执行。
-
异步执行dispatch函数会直接返回,Block函数我们可以认为它会在下一帧加入队列,并根据所在队列目前的任务情况无限下一帧执行,从而不会阻塞当前外部任务的执行。同时,只有异步执行才有开辟新线程的必要,但是异步执行不一定会开辟新线程。
-
只要是队列,肯定是FIFO(先进先出),但是谁先执行完要看第1条。
-
只要是串行队列,肯定要等上一个任务执行完成,才能开始下一个任务。但是并行队列当上一个任务开始执行后,下一个任务就可以开始执行。
-
想要开辟新线程必须让任务在异步执行,想要开辟多个线程,只有让任务在并行队列中异步执行才可以。执行方式和队列类型多层组合在一定程度上能够实现对于代码执行顺序的调度。
-
同步+串行:未开辟新线程,串行执行任务;同步+并行:未开辟新线程,串行执行任务;异步+串行:新开辟一条线程,串行执行任务;异步+并行:开辟多条新线程,并行执行任务;在主线程中同步使用主队列执行任务,会造成死锁。
-
对于多核CPU来说,线程数量也不能无限开辟,线程的开辟同样会消耗资源,过多线程同时处理任务并不是你想像中的人多力量大。
QoS
QoS的全称是quality of service。刚刚上文也使用到,在Swift 3中,它是一个结构体,用来制定队列或者任务的重要性
何为重要性呢?就是当资源有限的时候,优先执行哪些任务。这些优先级包括CPU时间,数据IO等等,也包括ipad muiti tasking(两个App同时在前台运行)。
- 通常使用QoS为以下四种
从上到下优先级依次降低。
依次含义如下:
- 1.User Interactive 和用户交互相关,比如动画等等优先级最高。比如用户连续拖拽的计算
- 2.User Initiated 需要立刻的结果,比如push一个ViewController之前的数据计算
- 3.Utility 可以执行很长时间,再通知用户结果。比如下载一个文件,给用户下载进度。
- 4.Background 用户不可见,比如在后台存储大量数据
在GCD中,指定QoS有以下两种方式
方式一,创建一个指定QoS的queue
let backgroundQueue = DispatchQueue(label: "com.leo.backgroundQueu", qos: .background)
backgroundQueue.async {
//在QoS为background下运行
}
方式二,在提交block的时候,指定QoS
queue.async(qos: .background) {
//在QoS为background下运行
}
after(延迟执行)
GCD可以通过asyncAfter和syncAfter来提交一个延迟执行的任务
比如
let deadline = DispatchTime.now() + 2.0
NSLog("Start")
DispatchQueue.global().asyncAfter(deadline: deadline) {
NSLog("End")
}
可以看到,两秒后打印了End
2017-01-05 22:42:04.781 GCD[1617:36711] Start
2017-01-05 22:42:06.972 GCD[1617:36768] End
延迟执行还支持一种模式DispatchWallTime
let walltime = DispatchWallTime.now() + 2.0
NSLog("Start")
DispatchQueue.global().asyncAfter(wallDeadline: walltime) {
NSLog("End")
}
这里的区别就是
- DispatchTime 的精度是纳秒
- DispatchWallTime 的精度是微秒
Synchronization
通常,在多线程同时会对一个变量(比如NSMutableArray)进行读写的时候,我们需要考虑到线程的同步。举个例子:比如线程一在对NSMutableArray进行addObject的时候,线程二如果也想addObject,那么它必须等到线程一执行完毕后才可以执行。
- 实现这种同步有很多种机制:
比如用互斥锁:
lock.lock()
//Do something
lock.unlock()
使用锁有一个不好的地方就是:lock和unlock要配对使用,不然极容易锁住线程,没有释放掉。
使用GCD,队列同步有另外一种方式 - sync,讲属性的访问同步到一个queue上去,就能保证在多线程同时访问的时候,线程安全。
class MyData{
private var privateData:Int = 0
private let dataQueue = DispatchQueue(label: "com.leo.dataQueue")
var data:Int{
get{
return dataQueue.sync{ privateData }
}
set{
dataQueue.sync { privateData = newValue}
}
}
}
Barrier# (栅栏函数)
- barrier翻译过来就是屏障。在一个并行queue里,很多时候,我们提交一个新的任务需要这样做。
- queue里之前的任务执行完了新任务才开始
新任务开始后提交的任务都要等待新任务执行完毕才能继续执行
1.实现高效率的数据库访问和文件访问
2.避免数据竞争
典型的场景就是往NSMutableArray里addObject。
例如:
let concurrentQueue = DispatchQueue(label: "com.leo.concurrent", attributes: .concurrent)
concurrentQueue.async {
readDataTask(label: "1")
}
concurrentQueue.async {
readDataTask(label: "2")
}
concurrentQueue.async(flags: .barrier, execute: {
print("Task from barrier 1 begin")
sleep(3)
print("Task from barrier 1 end")
})
concurrentQueue.async {
readDataTask(label: "3")
}
concurrentQueue.async {
readDataTask(label: "4")
}
然后看到输出
17:14:19.690 Dispatch[15609:245546] Start data task1
17:14:19.690 Dispatch[15609:245542] Start data task2
17:14:22.763 Dispatch[15609:245546] End data task1
17:14:22.763 Dispatch[15609:245542] End data task2
17:14:22.764 Dispatch[15609:245546] Task from barrier 1 begin
17:14:25.839 Dispatch[15609:245546] Task from barrier 1 end
17:14:25.839 Dispatch[15609:245546] Start data task3
17:14:28.913 Dispatch[15609:245546] End data task3
执行的效果就是:barrier任务提交后,等待前面所有的任务都完成了才执行自身。barrier任务执行完了后,再执行后续执行的任务。
- 其中12 与 34 由于并行处理先后顺序不定. task.png