深度理解GCD线程死锁,队列,同步和异步,串行和并发

介绍GCD

可以先看看这个

“并发”指的是程序的结构,“并行”指的是程序运行时的状态
https://blog.csdn.net/sinat_35512245/article/details/53836580
并发是能力

并行是状态

并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计(cpu时间片轮转优先级调度)

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

任务和队列

看下最简单的GCD异步把任务加入全局并发队列的代码
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"任务");
});
  • 任务
    任务其实就是一段想要执行的代码,在GCD中就是Block,就是C代码的闭包实现,需要详细了解Block的请戳2分钟明白Block,因此,做法非常简单,例如reloadTableView 就可以加到这里去,问题在于任务的执行方式同步执行异步执行 ,这两者最简单可以概括为 是否具有开线程的能力
    展开来说就是是否会阻塞当前线程,如果和上面示例代码所示,是async,他不会阻塞当前线程,block里面的任务会在另一个线程执行,当前线程会继续往下走,如果是sync,那么Block里面的任务就会阻塞当前线程,该线程之后的任务都会等待block的任务执行完,如果比较耗时,线程就会处于假死状态
  • 队列
    上面讲的是任务的同步执行或者异步执行,那么队列就是用于任务存放,分别有串行队列并行队列
    队列都遵循FIFO(first in first out),串行队列根据先进先出的顺序取出来放到当前线程中,二并行队列会把任务取出来放到开辟的非当前线程,也就异步线程中,任务无限多的时候不会开无限个线程,会根据系统的最大并发数进行开线程

简单概括如下:
项目同步(sync)异步(async)
串行当前线程,顺序执行另一个线程,顺序执行
并发当前线程,顺序执行另一个线程,同时执行

可以看出同步和异步就是开线程的能力,同步执行必然一个个顺序执行在当前线程,而异步执行可以根据队列不同来确定顺序还是同步并发执行

队列的创建 和 简单API

  • 主队列:dispatch_get_main_queue(); 主线程中串行队列
  • 全局队列:dispatch_get_global_queue(0, 0); 全局并行队列
  • 自定义队列

     // 自定义串行队列
    dispatch_queue_create(@"custom name of thread", DISPATCH_QUEUE_SERIAL);
    // 自定义并发队列
    dispatch_queue_create(@"com.mkj.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
  • 最简单的API

    dispatch_sync(<#dispatch_queue_t  _Nonnull queue#>, ^(void)block)
    dispatch_async(<#dispatch_queue_t  _Nonnull queue#>, ^(void)block)
    

线程死锁1

NSLog(@"任务1");
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"任务2");
});
NSLog(@"任务3");

打印信息

2016-12-04 12:50:55.932 GCD[3020:116100] 任务1
(lldb) 
Exc_bad_INSTRUCTION报错

分析:
首先执行任务1,然后遇到dispatch_sync 同步线程,当前线程进入等待,等待同步线程中的任务2执行完再执行任务3,这个任务2是加入到mainQueue主队列中(上面有提到这个是同步线程),FIFO原则,主队列加入任务加入到队尾,也就是加到任务3之后,那么问题就来了,任务3等待任务2执行完,而任务2加入到主队列的时候,任务2就会等待任务3执行完,这个就赵成了死锁。
这里写图片描述

根据上面的描述,可以看到,当前线程中,如果开启同步,而且把任务加入到当前线程,那么当前线程就会阻塞
看下如下的案例
案例一
我们在当前线程中,开启同步等待,然后把任务加入到自定义的串行队列中,这个时候任务一执行完之后,程序等待,任务2被放入串行队列(不是当前队列主队列中),那么另外开辟的串行队列执行任务2,然后继续执行任务3,不会有死循环

dispatch_queue_t t = dispatch_queue_create("com.mikejing", DISPATCH_QUEUE_SERIAL);
    NSLog(@"任务1");
    dispatch_sync(t, ^{
        NSLog(@"任务2");
    });
    NSLog(@"任务3");
    2018-09-07 21:52:03.290198+0800 inherit[1640:22106] 任务1
    2018-09-07 21:52:03.290379+0800 inherit[1640:22106] 任务2
    2018-09-07 21:52:03.290450+0800 inherit[1640:22106] 任务3

案例二
依然是同步等待,我们这个时候把任务2放在全局并发队列里面,这个时候,一样同步等待,等待并发队列任务2执行完,再执行任务3,这里等待的是并发队列,不会阻塞当前线程
主线程中 同步并发队列

NSLog(@"任务1");
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"%@",[NSThread currentThread]);
        NSLog(@"任务2");
    });
    NSLog(@"任务3");
    2018-09-07 23:12:53.943102+0800 inherit[4350:71057] 任务1
    2018-09-07 23:12:53.943256+0800 inherit[4350:71057] <NSThread: 0x60c000079c80>{number = 1, name = main}
    2018-09-07 23:12:53.943345+0800 inherit[4350:71057] 任务2
    2018-09-07 23:12:53.943433+0800 inherit[4350:71057] 任务3

子线程中同步并发队列

//    dispatch_queue_t t = dispatch_queue_create("com.mikejing", DISPATCH_QUEUE_SERIAL);
//    // 异步串行
//    dispatch_async(t, ^{
//        NSLog(@"任务1%@",[NSThread currentThread]);
//        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
//            NSLog(@"任务2%@",[NSThread currentThread]);
//        });
//        NSLog(@"任务3%@",[NSThread currentThread]);
//    });
//    2018-09-07 22:11:50.201285+0800 inherit[2421:38097] 任务1<NSThread: 0x60400027d740>{number = 3, name = (null)}
//    2018-09-07 22:11:50.201442+0800 inherit[2421:38097] 任务2<NSThread: 0x60400027d740>{number = 3, name = (null)}
//    2018-09-07 22:11:50.201566+0800 inherit[2421:38097] 任务3<NSThread: 0x60400027d740>{number = 3, name = (null)}

案例二告诉我们,只要是同步,就不会开辟线程,无论是串行队列还是并发队列,都会等待,然后会有线程自身调度去执行串行中的任务或者并发列队中的任务,可以理解为,同步的前提下,串行队列和并发队列是一样的,因为同步,反正需要一个一个执行。

线程死锁2

dispatch_queue_t serialQueue = dispatch_queue_create("com.mkj.serialQueue", DISPATCH_QUEUE_SERIAL);

NSLog(@"任务1");
dispatch_async(serialQueue, ^{
    NSLog(@"任务2");
    dispatch_sync(serialQueue, ^{
        NSLog(@"任务3");
    });
    NSLog(@"任务4");
});
NSLog(@"任务5");

打印日志:

2016-12-04 13:10:06.587 GCD[3322:129782] 任务1
2016-12-04 13:10:06.588 GCD[3322:129782] 任务5
2016-12-04 13:10:06.588 GCD[3322:129815] 任务2
(lldb)  同样在这里报错停止

分析
1.这里用系统create的方法创建自定义线程,按顺序先执行任务1

2.然后遇到一个异步线程,把任务2,同步线程(包含任务3),任务4这三个东西看成一体放到自定义的串行队列中,由于是异步线程,直接执行下一个任务5,因此异步线程的任务2和任务5不确定谁先谁后,但是任务1 任务2 任务5这三个东西必定会打印出来

3.看下异步线程里面,都放置在自定义的串行队列中,任务2之后遇到一个同步线程,那么线程阻塞,执行同步线程里面的任务3,由于这个队列里面放置的任务4按第二步里面的顺序率先加入进串行队列的,当同步线程执行的时候,里面的任务3是还是按照FIFO顺序加入到任务4之后,那么又造成了案例一里面的任务4等待任务3,任务3等待任务4的局面,又死锁了
这里写图片描述这里写图片描述

这里写图片描述

线程之间的调度,安全避开死锁

NSLog(@"任务1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"任务2");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"任务3");
    });
    NSLog(@"任务4");

});
NSLog(@"任务5");

打印日志:这里不会产生死锁,直接解释下如何调度

2016-12-04 13:34:56.136 GCD[3726:150720] 任务1
2016-12-04 13:34:56.137 GCD[3726:150720] 任务5
2016-12-04 13:34:56.137 GCD[3726:150765] 任务2
2016-12-04 13:34:56.142 GCD[3726:150720] 任务3
2016-12-04 13:34:56.143 GCD[3726:150765] 任务4

1最外层分析,首先执行任务1,然后遇到异步线程,不阻塞,直接任务5,由于异步线程有任务2,直接输出

2.这个异步线程是全局并发队列,但是里面又遇到了同步线程,也就是说任务2执行完之后线程阻塞,这个同步线程的任务3是加到mainQueue中的,也就是任务5之后

3.前面已经执行完了任务125或152,那么阻塞的3可以顺利执行,执行完3之后就可以顺利地执行任务4

这里写图片描述

线程死锁4

dispatch_async(dispatch_get_global_queue(0, 0), ^{
   NSLog(@"任务1");
   dispatch_sync(dispatch_get_main_queue(), ^{
       NSLog(@"任务2");
   });
   NSLog(@"任务3");
   });
NSLog(@"任务4");
while (1) {

}
NSLog(@"任务5");
这里会有警告
code will never be executed 埋了隐患,编译器还是还强的,注意看就能避免很多死锁

打印日志:

2016-12-04 13:52:09.597 GCD[3976:163387] 任务1
2016-12-04 13:52:09.597 GCD[3976:163302] 任务4    

分析:
1.一开始就是一个异步线程,任务4,死循环和任务5,这里注定了任务5不会被执行,如果其他线程有任务加到主线程中来,那么必定卡死

2.肯定能打印1和4,然后异步线程中遇到同步线程,同步线程的任务是加到mainQueue中的,也就是加到任务5之后,我擦,这肯定炸了,任务5是不会执行的,因此,任务3肯定不会被执行,而且异步线程里面的是同步阻塞的,那么任务3之后的代码肯定也不会执行

3.这里main里面的死循环理论上是不会影响异步线程中的任务1,2,3的,但是任务2是要被加到主队列执行的,那么忧郁FIFO的原理,导致不会执行任务2,那么就死锁了

这里写图片描述

总结

很多死锁造成的原因第一点是在主线程或者子线程中遇到了一个同步线程,如果这个同步线程把任务加到自己所在线程的同步队列里面就会死锁(mainQueue也是同步队列)

dispatch_sync(来一个同一个同步队列或者mainQueue, <#^(void)block#>)

这种情况下及其容易死锁,千万要小心

能明白就能让上面的线程死锁例子二进行解锁

死锁例子二:
解锁1 新增一个串行队列

dispatch_queue_t serialQueue = dispatch_queue_create("com.mkj.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue1 = dispatch_queue_create("com.mkj.serialQueue1", DISPATCH_QUEUE_SERIAL);

NSLog(@"任务1");
dispatch_async(serialQueue, ^{
    NSLog(@"任务2");
    dispatch_sync(serialQueue1, ^{
        NSLog(@"任务3");
    });
    NSLog(@"任务4");
});
NSLog(@"任务5");

解锁2 用全局并发队列

dispatch_queue_t serialQueue = dispatch_queue_create("com.mkj.serialQueue", DISPATCH_QUEUE_SERIAL);

NSLog(@"任务1");
dispatch_async(serialQueue, ^{
    NSLog(@"任务2");
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务3");
    });
    NSLog(@"任务4");
});
NSLog(@"任务5");

都能正常打印出

15234 或者 12345这个异步的2和5无法确定,所有有几种可能

总结下:
1.死锁的情况,当前线程无论是主线程还是子线程,只要是串行队列,我们继续sync同步等待,然后加入block任务,这个时候就会死锁,如上图
2.同步并发队列,由于同步的限制,只会在当前线程执行,因此并发和串行队列都是一样的
3.当我们async回到getmainqueue的时候,实际上是在主线程队列最后追加任务,档主线程其他任务完成后才回去执行回调的block
4.案例死锁第一个,当主线程调用sync同步等待任务,而且继续加入到主线程中的时候,就会死锁,一种解锁办法就是把sync的队列换成自定义串行队列或者并发队列即可
5.按我现在的理解,开不开线程取决于同步还是异步,同步不开,异步开,开几条取决于队列,串行队列开一条,并发队列开多条(取决于cpu)
关于同步异步:

6.dispatch_sync是同步函数,不具备开启新线程的能力,交给它的block,只会在当前线程执行,不论你传入的是串行队列还是并发队列,并且,它一定会等待block被执行完毕才返回。
dispatch_async是异步函数,具备开启新线程的能力,但是不一定会开启新线程(例如async……get_main_queue就不会开线程,其他串行开一条,并发队列开多条),交给它的block,可能在任何线程执行,开发者无法控制,是GCD底层在控制。它会立即返回,不会等待block被执行。
注意:以上两个知识点,有例外,那就是当你传入的是主队列,那两个函数都一定会安排block在主线程执行。记住,主队列是最特殊的队列

7.以上都是最简单的理解,任务都是同步任务,那么衍生出来一个超级大问题,如果Block里面的任务是异步网络请求,如何控制先后顺序?如果Block任务里面还嵌套异步任务,因为并发队列里面的任务,只是负责打印和发送请求的操作,异步回调数据是不归队列管的。
一道阿里的面试题
使用GCD如何实现A,B,C三个任务并发,完成后执行任务D?
不是让你打印同步任务,而且网络并发任务的先后依赖如何形成?
另开一篇深入介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值