GCD 细细的读
目录
<a name="前言"></a>前言
在日常app开发中多线程的使用是难免的,既然躲不过,干嘛不好好的享受? 本文将对GCD进行全面的解析。
<a name="为什么选择GCD?"></a>为什么选择GCD?
它是苹果在 iOS 4 时推出的,为多核的并行运算提出的, 以c语言编写的解决方案。高效,自动管理是它的优点。
<a name="串行队列、并行队列、同步、异步"></a>串行队列、并行队列、同步、异步
串行队列:放到串行队列的任务,GCD 会FIFO(先进先出)地取出来一个,执行一个,然后取下一个,这样一个一个的执行。
并行队列
- 为异步执行时:放到并行队列的任务,GCD 也会FIFO的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。
- 为同步时:和串行队列基本一致。
同步:会先阻塞当前线程,直到任务完成。
异步:会另外开线程处理,不会阻塞当前线程。
串行情况下的 同步和异步
func testFive() {
//串行队列
let qe = DispatchQueue.init(label: "concurrentQueue")
print("当前线程",Thread.current)
//同步
qe.sync {
for i in 0..<5 {
print("🐩 - \(i)",Thread.current)
}
}
qe.sync {
for i in 0..<5 {
print("🐒 - \(i)",Thread.current)
}
}
//异步
qe.async {
for i in 0..<5 {
print("🇭🇰 - \(i)",Thread.current)
}
}
qe.async {
for i in 0..<5 {
print("🇲🇴 - \(i)",Thread.current)
}
}
}
当前线程 <NSThread: 0x60800006d540>{number = 1, name = main}
🐩 - 0 <NSThread: 0x60800006d540>{number = 1, name = main}
🐩 - 1 <NSThread: 0x60800006d540>{number = 1, name = main}
🐩 - 2 <NSThread: 0x60800006d540>{number = 1, name = main}
🐩 - 3 <NSThread: 0x60800006d540>{number = 1, name = main}
🐩 - 4 <NSThread: 0x60800006d540>{number = 1, name = main}
🐒 - 0 <NSThread: 0x60800006d540>{number = 1, name = main}
🐒 - 1 <NSThread: 0x60800006d540>{number = 1, name = main}
🐒 - 2 <NSThread: 0x60800006d540>{number = 1, name = main}
🐒 - 3 <NSThread: 0x60800006d540>{number = 1, name = main}
🐒 - 4 <NSThread: 0x60800006d540>{number = 1, name = main}
🇭🇰 - 0 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇭🇰 - 1 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇭🇰 - 2 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇭🇰 - 3 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇭🇰 - 4 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇲🇴 - 0 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇲🇴 - 1 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇲🇴 - 2 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇲🇴 - 3 <NSThread: 0x600000078f40>{number = 3, name = (null)}
🇲🇴 - 4 <NSThread: 0x600000078f40>{number = 3, name = (null)}
结论:可以看出,在串行情况下,不管是异步还是同步都是一个个按照顺序执行。唯一的区别就是异步单独开了个线程
并行情况下的 同步和异步
func testFive() {
//其实就是初始化函数里,多了个concurrent 的参数
let qe = DispatchQueue.init(label: "concurrentQueue", attributes:.concurrent)
print("当前线程",Thread.current)
//同步
qe.sync {
for i in 0..<5 {
print("🐩 - \(i)",Thread.current)
}
}
qe.sync {
for i in 0..<5 {
print("🐒 - \(i)",Thread.current)
}
}
//异步
qe.async {
for i in 0..<5 {
print("🇭🇰 - \(i)",Thread.current)
}
}
qe.async {
for i in 0..<5 {
print("🇲🇴 - \(i)",Thread.current)
}
}
}
当前线程 <NSThread: 0x60000006b380>{number = 1, name = main}
🐩 - 0 <NSThread: 0x60000006b380>{number = 1, name = main}
🐩 - 1 <NSThread: 0x60000006b380>{number = 1, name = main}
🐩 - 2 <NSThread: 0x60000006b380>{number = 1, name = main}
🐩 - 3 <NSThread: 0x60000006b380>{number = 1, name = main}
🐩 - 4 <NSThread: 0x60000006b380>{number = 1, name = main}
🐒 - 0 <NSThread: 0x60000006b380>{number = 1, name = main}
🐒 - 1 <NSThread: 0x60000006b380>{number = 1, name = main}
🐒 - 2 <NSThread: 0x60000006b380>{number = 1, name = main}
🐒 - 3 <NSThread: 0x60000006b380>{number = 1, name = main}
🐒 - 4 <NSThread: 0x60000006b380>{number = 1, name = main}
🇭🇰 - 0 <NSThread: 0x608000078640>{number = 3, name = (null)}
🇲🇴 - 0 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
🇭🇰 - 1 <NSThread: 0x608000078640>{number = 3, name = (null)}
🇲🇴 - 1 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
🇭🇰 - 2 <NSThread: 0x608000078640>{number = 3, name = (null)}
🇲🇴 - 2 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
🇭🇰 - 3 <NSThread: 0x608000078640>{number = 3, name = (null)}
🇲🇴 - 3 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
🇭🇰 - 4 <NSThread: 0x608000078640>{number = 3, name = (null)}
🇲🇴 - 4 <NSThread: 0x6000000767c0>{number = 4, name = (null)}
结论:可以看出,在并行情况下,同步时和串行一致。异步则会开多个线程并行执行。
当然,如果你还是有点晕的话可以看下面这个表格,一般情况都可以用下面的表格解释(例外看下面的注意点!)
同步执行 | 异步执行 | |
---|---|---|
串行队列 | 当前线程,一个一个执行 | 其他线程,一个一个执行 |
并行队列 | 当前线程,一个一个执行 | 开很多线程,一起执行 |
注意点:经过测试,得到的经验:(1)主线程中的其他队列异步都能分出线程(系统线程资源没用完前),(2)分线程中的其他队列异步不一定分出线程;(3)主线程中的主队列异步不能分出线程。
<a name="线程死锁解析"></a>线程死锁解析
一、串行队列
- 1、主线程死锁
func syncFirst() {
print("MainQueue-Task1",Thread.current)
DispatchQueue.main.sync {
print("MainQueue-Task2-sync",Thread.current)
}
print("MainQueue-Task3",Thread.current)
}
输出:1
MainQueue-Task1 <NSThread: 0x60000006e640>{number = 1, name = main}
解析:
在主队列存在3个任务(任务1,同步任务2,任务3),按照正常的队列的顺序执行顺序应该为
任务1-->同步任务2-->任务3;但是因为同步任务2的存在,导致主线程阻塞,且同步任务2被抽出重新入队列,于是同步任务2,在主队列中排在了任务3的后面。那么问题来了,现在有两条线 1: 任务1-->同步任务2-->任务3 // 2: 任务1-->任务3-->同步任务2 于是主线程就会因为互相等待不知道先执行哪个而完全阻塞。
如下图所示
gcd-deadlock-1.png- 2、其他队列的阻塞
func syncSec() {
let queue = DispatchQueue.init(label: "MyQueue")
print("MainQueue-Task1",Thread.current)
queue.async {
print("MyQueue-Task2",Thread.current)
queue.sync(execute: {
print("MyQueue-Task3",Thread.current)
})
print("MyQueue-Task4",Thread.current)
}
print("MainQueue-Task5",Thread.current)
}
输出:1 -(2/5)
MainQueue-Task1 <NSThread: 0x608000071400>{number = 1, name = main}
MainQueue-Task5 <NSThread: 0x608000071400>{number = 1, name = main}
MyQueue-Task2 <NSThread: 0x60000007d200>{number = 3, name = (null)}
解析:
在主队列存在3个任务(任务1,异步任务,任务5),那么毫无疑问任务1最先执行,因中间隔着是异步线程,故任务5可以和异步线程同时开始执行;MyQueue串行队列中存在3个任务(同步任务2,同步任务,同步任务),任务2先执行,然后遇到了同步操作,那么还是和上个问题一样,同步任务3和任务4之间相互等待,造成 线程<NSThread: 0x60000007d200>的完全阻塞,程序停止;
如下图所示
线程死锁2.png- 3、错觉:只要串行队列中有同步操作,就会立马死锁?
看完第一个和第二个的例子是不是有一种错觉:只要串行队列中有同步操作,就会立马死锁?其实不然,下面的例子就证明了有串行同步操作也是可以的。
func syncThree() {
let qe = DispatchQueue.init(label: "MyQueue")
print("MainQueue-Task1",Thread.current)
qe.sync {
print("MyQueue-Task2",Thread.current)
print("MyQueue-Task3",Thread.current)
}
print("MainQueue-Task4",Thread.current)
}
输出:1-2-3 ,且没有推出程序
MainQueue-Task1 <NSThread: 0x600000079000>{number = 1, name = main}
MyQueue-Task2 <NSThread: 0x600000079000>{number = 1, name = main}
MyQueue-Task3 <NSThread: 0x600000079000>{number = 1, name = main}
MainQueue-Task4 <NSThread: 0x600000079000>{number = 1, name = main}
解析:
主队列中3个任务(任务1,同步任务,任务4),MyQueue队列中两个任务(任务2,任务3)。主线程首选执行主队列中的任务1,然后遇到了同步任务,关键来了!因同步任务是MyQueue的全部任务集合,故不会和主队列进行冲突,而是按照官方描述的,遇到同步任务要执行完任务中的事情,于是任务2和任务3相继被执行,之后主队列的任务4出队列被执行! 顺序为:1->2->3->4。
如下图所示
线程死锁3.png- 4、串行队列下到底怎样的情况下才会造成死锁?
func syncFour() {
let qe = DispatchQueue.init(label: "MyQueue")//, attributes:.concurrent)
print("MainQueue-Task1",Thread.current)
qe.sync {
print("MyQueue-Task2",Thread.current)
qe.sync {
print("MyQueue-Task3",Thread.current)
}
print("MyQueue-Task4",Thread.current)
}
print("MainQueue-Task5",Thread.current)
}
输出 1-2
MainQueue-Task1 <NSThread: 0x60800006f240>{number = 1, name = main}
MyQueue-Task2 <NSThread: 0x60800006f240>{number = 1, name = main}
解析:
主队列中3个任务(任务1,同步任务,任务5),MyQueue队列中三个任务(任务2,同步任务3,任务4)。
主线程首先执行任务1,之后遇到MyQueue的同步任务,跳到MyQueue中执行任务2,又遇到了MyQueue的同步任务,那么同步任务中的任务3被推入队列中,导致任务3跑到任务4的后面,导致了任务3和任务4的相互等待,造成死锁!
我们回到第一个例子的代码中再思考一下,和这段代码有没有共同点??其实我们可以理解为下面这段伪代码
//伪代码,为了说明
MainQueue.sync{
viewDidload{
....
syncFirst();
....
}
}
func syncFirst() {
print("MainQueue-Task1",Thread.current)
DispatchQueue.main.sync {
print("MainQueue-Task2-sync",Thread.current)
}
print("MainQueue-Task3",Thread.current)
}
是不是明白了什么?整个主线程已经是在串行同步的条件下了,
所以我么可以总结一下串行队列的死锁情况:串行队列中有属于自身队列的同步操作,就会立马死锁!或者说 任何一个串行队列,不能添加两个本队列的同步操作!
如下图所示
线程死锁4.png二、并行队列
- 1、并行队列下的同步
func syncFive() {
let qe = DispatchQueue.init(label: "MyQueue", attributes:.concurrent)
print("MainQueue-Task1",Thread.current)
qe.sync {
print("MyQueue-Task2",Thread.current)
qe.sync {
print("MyQueue-Task3",Thread.current)
}
print("MyQueue-Task4",Thread.current)
}
print("MainQueue-Task5",Thread.current)
}
输出:1-2-3-4-5
MainQueue-Task1 <NSThread: 0x608000076480>{number = 1, name = main}
MyQueue-Task2 <NSThread: 0x608000076480>{number = 1, name = main}
MyQueue-Task3 <NSThread: 0x608000076480>{number = 1, name = main}
MyQueue-Task4 <NSThread: 0x608000076480>{number = 1, name = main}
MainQueue-Task5 <NSThread: 0x608000076480>{number = 1, name = main}
解析
主队列中3个任务(任务1,同步任务,任务5),MyQueue队列中三个任务(任务2,同步任务3,任务4)。
主线程首先执行任务1,遇到MyQueue的同步任务跳到MyQueue队列中,执行任务2,此时遇到了MyQueue自己队列的同步任务3,因是并行情况所有所以直接执行(ps:至于为什么直接过去了,我还没找到原理般的解释,先相信知乎上这篇文章上第一个回答所说的,如果大家知道原理,可以告诉我。)
执行完成后再回到主队列,执行任务5。
如下图所示
线程死锁5.png- 2、 并行下也是有可能出现死锁的,别大意
func testThree() {
// 全局线程
let queueGlobal = DispatchQueue.global()
// 主线程
let queueMain = DispatchQueue.main
print("MainQueue-Task1",Thread.current)
//全局异步
queueGlobal.async {
print("GlobalQueue-Task2 ",Thread.current)
//主线程同步,因主线程阻塞,block内容和while循环相互等待
queueMain.sync(execute: {
print("GlobalQueue-Task3 ",Thread.current)
})
print("GlobalQueue-Task4",Thread.current)
}
print("MainQueue-Task5",Thread.current)
sleep(3);
print("MainQueue-Task6")
}
输出 1-(5/2)-6-3-4
MainQueue-Task1 <NSThread: 0x60000007d4c0>{number = 1, name = main}
MainQueue-Task5 <NSThread: 0x60000007d4c0>{number = 1, name = main}
GlobalQueue-Task2 <NSThread: 0x608000269680>{number = 3, name = (null)}
MainQueue-Task6
GlobalQueue-Task3 <NSThread: 0x60000007d4c0>{number = 1, name = main}
GlobalQueue-Task4 <NSThread: 0x608000269680>{number = 3, name = (null)}
解析
看图就知道这个比较麻烦!主队列中6个任务(任务1,异步任务,任务5,休眠任务,任务6,和后面入对的任务3),GlobleQueue队列中三个任务(任务2,同步任务,任务4)。主线程执行任务1,遇到异步则分出线程执行任务2,同时主线程继续执行任务5,所以任务2和5位置不一定。现在有两条线程,主线程上进入了3秒休眠。分线程上因遇到主线程的同步任务,主线程为串行同步队列,故需要把任务3加入到主线程的队尾,在任务6之后。休眠结束后,主线程继续任务6和任务3,任务3执行完之后,同步任务才得以完成,所以任务4才能执行。有点绕,要消化一下!
如下图所示
线程死锁6.png<a name="DispatchQueue的使用"></a>DispatchQueue的使用
初始化
let queue = DispatchQueue.init(label:String,qos:DispatchQoS,attributes:DispatchQueue.Attributes,autoreleaseFrequency:DispatchQueue.AutoreleaseFrequency, target: DispatchQueue?)
属性
- label 队列的标识 (required字段,下面都是option字段)
- qos (quality of server) 服务质量,也可以说是优先级,优先级高的得到的资源和权限就越高!下面为从高到低的优先级
- userInteractive
- userInitiated
- default
- utility
- background
- unspecified
- attributes
- concurrent: 并行自动模式
- initiallyInactive: 还是串行,但需要手动启动 (如queue.activate())
- autoreleaseFrequency
- inherit 继承
- workItem 工作项目
- never
- target 新的一个queue。注意:如果传这个参数的话,前面的配置无效,全部按照新传入的queue本身的配置。(亲测)
<a name="Dispatchgroup的使用"></a>Dispatchgroup的使用
初始化
let group = DispatchGroup.init()
实战
下面的一段代码很好的解决了多个网络请求并行的问题。
func testGroup() {
let group = DispatchGroup.init()
let queueOne = DispatchQueue.init(label: "queueOne",attributes:.concurrent)
let firstTime = NSDate.init().timeIntervalSince1970
print("主线程",Thread.current)
print("开始时间",firstTime)
//这里和下面的三段模拟日常的网络请求
group.enter()
queueOne.async(group: group) {
sleep(1) //模拟请求延迟
print("1结束")
group.leave()
}
group.enter()
queueOne.async(group: group) {
sleep(2)
print("2结束")
group.leave()
}
group.enter()
queueOne.async(group: group) {
sleep(3)
print("3结束")
group.leave()
}
group.notify(queue: DispatchQueue.main) {
let secTime = NSDate.init().timeIntervalSince1970
print("所有异步执行结束时间",secTime)
print("主线程",Thread.current)
}
}
输出:
主线程 <NSThread: 0x60000006e800>{number = 1, name = main}
开始时间 1487831647.5901
1结束
2结束
3结束
所有异步执行结束时间 1487831650.66429
主线程 <NSThread: 0x60000006e800>{number = 1, name = main}
解析
首先先创建一个group和一个并行的queue;然后往group里添加三段 并行queue异步的模拟网络请求,分别延迟1s,2s,3s。看输出你就可以看出,整个三段请求的时间是按照 请求时间最大的一段来决定的,所以是3s的时间。等所有的请求都完成之后,就会执行group的notify回调,传人主线程,就可以刷新UI拉。
注意点
enter 和 leave实际执行的次数得是1:1的,不然就会crash。所以我们平时就可以在网络请求的成功和失败block各放一个leave。因其要么失败要么成功,比例还是1:1。
<a name="DispatchSourceTiemr的使用"></a>DispatchSourceTiemr的使用
初始化
(评论中有朋友遇到过 定时器不执行的问题,是因为 没把 定时器全局化 。如:)
var timer:DispatchSourceTimer? 感谢XIAODAO同学 的建议,哈哈)
let timer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags.strict, queue: DispatchQueue.main)
当然也可以简单的初始化:
let timer = DispatchSource.makeTimerSource()//默认了主线程
注意点
timer要全局定义,上面那样局部定义初始化是不会执行的
属性
- flags 一般不用传
- queue 定时器运行的队列,传入主队列 在主线程执行;不传则默认在分线程
主要方法
-
scheduleOneshot 循环单次
-
scheduleRepeating 重复循环
两个方法的属性
- wallDeadline 延迟执行
- leeway 允许的误差
- deadline 执行的时间
- interval 循环模式时的间隔时间
来看几段代码
单次执行
func runOnce() {
print("主线程",Thread.current)
//创建主线程下的定时器
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
//单次执行
timer?.scheduleOneshot(deadline: DispatchTime.now(), leeway: .milliseconds(10))
//执行的代码块
timer?.setEventHandler {
print("单次执行",Thread.current)
}
//继续,用activate()也行
timer?.resume()
}
输出:
主线程 <NSThread: 0x6080000740c0>{number = 1, name = main}
单次执行 <NSThread: 0x6080000740c0>{number = 1, name = main}
多次执行
func runMul() {
print("主线程",Thread.current)
创建分线程下的定时器
timer = DispatchSource.makeTimerSource()
//循环执行,马上开始,间隔为1s,误差允许10微秒
timer?.scheduleRepeating(deadline: DispatchTime.now(), interval: .seconds(1), leeway: .milliseconds(10))
//执行内容
timer?.setEventHandler {
print("1 second interval",Thread.current)
}
//激活
timer?.activate()
}
输出:
主线程 <NSThread: 0x60000006f0c0>{number = 1, name = main}
1 second interval <NSThread: 0x60000006f0c0>{number = 1, name = main}
1 second interval <NSThread: 0x60000006f0c0>{number = 1, name = main}
1 second interval <NSThread: 0x60000006f0c0>{number = 1, name = main}
...
...
取消执行
func runCancel() {
print("主线程",Thread.current)
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
timer?.scheduleRepeating(deadline: DispatchTime.now(), interval: .seconds(1), leeway: .milliseconds(10))
timer?.setEventHandler {
print("1 second interval",Thread.current)
sleep(2)
print("after sleep")
}
timer?.activate()
//主队列创建的情况下 如果在这里直接cancel(),handler里面的内容是不能执行的。如果是默认的分线程,则是可以的,至于为什么,我前面多线程的时候解释过了哈。
//timer?.cancel()
//最好都这样取消
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: {
self.timer?.cancel()
})
}
输出:
主线程 <NSThread: 0x6080000644c0>{number = 1, name = main}
1 second interval <NSThread: 0x60000006f000>{number = 3, name = (null)}
after sleep
<a name="延迟执行"></a>延迟执行
GCD的延迟执行一般来说可以分为两大类
- 1、 队列直接asyncAfter
- 2、 定时器
如下代码所示
func testDelay() {
let queue = DispatchQueue.init(label: "myqueue")
let delayTime = DispatchTime.now() + 2.0
let delayTimeTimer = DispatchWallTime.now() + 2.0
print("before delay")
//第一种
queue.asyncAfter(deadline: delayTime) {
print("这是 asyncAfter delay")
}
//第二种
let workItem = DispatchWorkItem.init {
print("这是 asyncAfter workItem delay")
}
queue.asyncAfter(deadline: delayTime, execute: workItem)
//第三种
timer = DispatchSource.makeTimerSource()
timer?.scheduleOneshot(wallDeadline:delayTimeTimer)
timer?.setEventHandler(handler: {
print("这是 timer delay")
})
timer?.activate()
}
输出:
before delay
这是 timer delay
这是 asyncAfter delay
这是 asyncAfter workItem delay
总结
这是我第一次写这么长的博客,把自己都感动了!感觉还不错,还会继续下去。针对本文的观点,如有错误点,烦请指出!
本文引用以下文章的部分观点:五个案例让你明白GCD死锁、关于iOS多线程,你看我就够了、
作者:灯泡虫