iOS

GCD(一) 队列、任务、串行、并发

2019-04-30  本文已影响153人  左耳钉zed

本文是GCD多线程编程基础内容的小结,通过本文,你可以了解到:

GCD

Apple为了让开发者更加容易的使用设备上的多核CPU,苹果在 OS X 10.6 和 iOS 4 中引入了 Grand Central Dispatch(GCD),它是 Apple 开发的一个多核编程的较新的解决方法,它主要用于优化应用程序以支持多核处理器,它是一个在线程池模式的基础上执行的并发任务,是我们平常开发中最常见的一种多线程编程方式

测试代码在这

多线程基本概念

进程与线程

对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

串行和并发

并发就是多个任务在执行的过程中,时间互相重叠,一个任务执行没结束,另一个已经开始。

串行就是任务一个一个的执行,时间上不相互重叠,一个任务执行结束,下一个任务才能开始执行。

GCG队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的后端进入插入操作,在表的前端进行删除操作,即遵循FIFO原则。

GCD中的队列(Dispatch Queue)就是指用来执行任务的等待队列,当我们添加任务到队列之后,开发者不用再直接跟线程打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。

GCD中的队列可以分为以下2种:

在我们平时的开发中,还有2种我们最常见的,也是使用频率最高的队列:

GCD任务

任务就是你要在线程中执行的代码,在GCD中是用Block来定义任务的,是用起来非常灵活便捷。

GCD中执行任务的方式有两种:同步执行(sync)与异步执行(async)

GCD的使用步骤

这个就跟赵本山跟宋丹丹的小品《钟点工》里提出的把大象装进冰箱的经典问题一样,都是分三步:

把大象装进冰箱

  1. 把冰箱门打开
  2. 把大象装进去
  3. 把冰箱门关上

GCD使用步骤

  1. 创建或获取一个队列
  2. 定制需要执行的任务
  3. 将任务追加到队列

创建或获取一个队列

定制需要执行的任务

GCD种的任务其实就是一个Block,就是我们俗称的代码块,在这个代码块里面,把我们需要做的事情就是,将我们的任务代码加入到这个block中

void (^block)(void) = ^{
        NSLog(@"执行任务");
        for (int i = 0; i<100; i++) {
            NSLog(@"%d",i);
        }
        NSLog(@"Thread:%@",[NSThread currentThread]);
    };

将任务追加到队列

GCD提供了2个方法用于将任务追加到队列:

  1. dispatch_sync 使用同步执行的方式追加到队列
  2. dispatch_async 使用异步的方式追加到队列
//三、将任务增加到队列中
dispatch_async(globalQueue, block);

GCD的基本使用

前面我们已经介绍了两种基本队列(串行队列与并发队列),两种特殊队列(主队列与全局并发队列),两种任务执行方式(同步执行与异步执行),所以,我们就有了8中不同的组合方式,不过由于全局并发队列跟普通并发队列的性质是差不多的,所以,我们就有6中不同的组合,接下来,我们从3个角度来观察这6种组合方式的效果:

三个角度

  1. 是否开启线程
  2. 任务是按序执行还是交替(同时)执行
  3. 是否阻塞当前线程

六种组合方式

  1. 同步执行+并发队列
  2. 异步执行+并发队列
  3. 同步执行+串行队列
  4. 异步执行+串行队列
  5. 同步执行+主队列
  6. 异步执行+主队列

同步执行+并发队列

#pragma mark - 同步执行+并发队列
/*
 * 特点:
 * 1.在当前线程中执行任务,不会开启新线程
 * 2.按序执行任务,执行行完一个任务,再执行下一个任务
 * 3.会阻塞当前线程
 */
- (IBAction)executeSyncConcurrencyTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"SyncConcurrencyTask---begin");
    
    dispatch_sync(self.concurrentQueue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_sync(self.concurrentQueue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_sync(self.concurrentQueue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"SyncConcurrencyTask---end");
    NSLog(@"*********************************************************");
}

执行结果如下:

2019-04-22 13:42:07.265811+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] CurrentThread---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:07.265945+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] SyncConcurrencyTask---begin
2019-04-22 13:42:09.266681+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 1---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:11.268049+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 1---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:12.299360+0800 GCD(一) 队列、任务、串行、并发[8668:1868133] XPC connection interrupted
2019-04-22 13:42:13.269544+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 2---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:15.270540+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 2---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:17.271936+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 3---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:19.273478+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 3---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:19.273713+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] SyncConcurrencyTask---end
2019-04-22 13:42:19.273857+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] *********************************************************

通过我们的代码测验,可以看出:

  1. 所有的任务都是在主线程(当前线程)中执行的,并没有开启新的线程,这也说明了同步执行的一个特性:同步执行任务不具备开启新线程的能力。

  2. 任务1、任务2、任务3是按顺序执行的,并没有出现并发执行的情况,这是因为虽然并发队列具备同时执行多个任务的能力,但是由于是同步执行不具备开启新线程的能力,所以,即使任务被追加到了并发队列,它也没有办法去开启新的线程,只能在当前线程中执行任务。

  3. 从我们的log中可以看出,我们所有的任务都是在 beginend之间的,所以说,它会阻塞当前线程,等待队列中的任务执行结束,才会继续执行下面的代码。

异步执行+并发队列

#pragma mark - 异步执行+并发队列
/*
 * 特点:
 * 1.开启多个新线程执行任务
 * 2.任务交替(同时)执行
 * 3.不会阻塞当前线程
 */
- (IBAction)executeAsyncConcurrencyTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"AsyncConcurrencyTask---begin");
    
    dispatch_async(self.concurrentQueue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_async(self.concurrentQueue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_async(self.concurrentQueue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"AsyncConcurrencyTask---end");
    NSLog(@"*********************************************************");
}

执行结果如下:

2019-04-22 14:32:14.941222+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] CurrentThread begin---<NSThread: 0x6000000cea40>{number = 1, name = main}
2019-04-22 14:32:14.941405+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] AsyncConcurrencyTask---begin
2019-04-22 14:32:14.941608+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] CurrentThread end---<NSThread: 0x6000000cea40>{number = 1, name = main}
2019-04-22 14:32:14.941757+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] AsyncConcurrencyTask---end
2019-04-22 14:32:14.941894+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] *********************************************************
2019-04-22 14:32:16.945860+0800 GCD(一) 队列、任务、串行、并发[9417:2009294] 1---<NSThread: 0x6000000a2380>{number = 4, name = (null)}
2019-04-22 14:32:16.945909+0800 GCD(一) 队列、任务、串行、并发[9417:2011461] 3---<NSThread: 0x6000000a2340>{number = 6, name = (null)}
2019-04-22 14:32:16.945909+0800 GCD(一) 队列、任务、串行、并发[9417:2011460] 2---<NSThread: 0x6000000aec00>{number = 5, name = (null)}
2019-04-22 14:32:18.951120+0800 GCD(一) 队列、任务、串行、并发[9417:2011460] 2---<NSThread: 0x6000000aec00>{number = 5, name = (null)}
2019-04-22 14:32:18.951121+0800 GCD(一) 队列、任务、串行、并发[9417:2011461] 3---<NSThread: 0x6000000a2340>{number = 6, name = (null)}
2019-04-22 14:32:18.951120+0800 GCD(一) 队列、任务、串行、并发[9417:2009294] 1---<NSThread: 0x6000000a2380>{number = 4, name = (null)}

通过我们的代码测验,可以看出:

  1. 除了在主线程执行的2个log任务之外,系统又开启了3个线程用于执行追加的三个任务,说明异步执行具备开启新线程的能力,并且并发队列可以开启多个线程,交替执行多个任务。
  2. 从我们的log中可以看到,begin的log之后,马上就是end的log,因此可以看出,它并不会阻塞当前线程,并不需要等待追加的任务执行完成。

同步执行+串行队列

#pragma mark - 同步执行+串行队列
/*
 * 特点:
 * 1.在当前线程中执行任务,不会开启新线程
 * 2.按序执行任务,执行行完一个任务,再执行下一个任务
 * 3.会阻塞当前线程
 */
- (IBAction)executeSyncSerialTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"SyncSerialTask---begin");
    
    dispatch_sync(self.serialQueue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_sync(self.serialQueue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_sync(self.serialQueue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"SyncSerialTask---end");
    NSLog(@"*********************************************************");
}

执行结果如下:

2019-04-22 15:02:52.760352+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] CurrentThread begin---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:52.760558+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] SyncSerialTask---begin
2019-04-22 15:02:54.761971+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 1---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:56.762653+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 1---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:58.764202+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 2---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:00.765234+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 2---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:02.766464+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 3---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.767966+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 3---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.768230+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] CurrentThread end---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.768379+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] SyncSerialTask---end
2019-04-22 15:03:04.768516+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] *********************************************************

通过我们的代码测验,可以看出:

  1. 所有的任务都是在主线程(当前线程)中执行的,并且是顺序执行的,没有开启新的线程。
  2. 从我们的log中可以看出,我们所有的任务都是在 beginend之间的,所以说,它会阻塞当前线程,等待队列中的任务执行结束,才会继续执行下面的代码。

异步执行+串行队列

#pragma mark - 异步执行+串行队列
/*
 * 特点:
 * 1.会开启一条新线程
 * 2.按序执行任务,执行行完一个任务,再执行下一个任务
 * 3.不会阻塞当前线程
 */
- (IBAction)executeAsyncSerialTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"AsyncSerialTask---begin");
    
    dispatch_async(self.serialQueue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_async(self.serialQueue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_async(self.serialQueue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"AsyncSerialTask---end");
    NSLog(@"*********************************************************");
}

执行结果如下:

2019-04-22 15:25:00.103488+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] CurrentThread begin---<NSThread: 0x6000019c9400>{number = 1, name = main}
2019-04-22 15:25:00.103734+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] AsyncSerialTask---begin
2019-04-22 15:25:00.103888+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] CurrentThread end---<NSThread: 0x6000019c9400>{number = 1, name = main}
2019-04-22 15:25:00.103986+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] AsyncSerialTask---end
2019-04-22 15:25:00.104091+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] *********************************************************
2019-04-22 15:25:02.108899+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 1---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:04.111910+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 1---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:06.116733+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 2---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:08.117706+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 2---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:10.122737+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 3---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:12.126742+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 3---<NSThread: 0x600001992fc0>{number = 4, name = (null)}

通过我们的代码测验,可以看出:

  1. 三个追加的任务都是在一个新的线程中执行的,在串行队列中异步执行任务,会开启一条新线程,由于队列是串行的,所以任务是按序执行的。
  2. 从我们的log中可以看到,begin的log之后,马上就是end的log,因此可以看出,它并不会阻塞当前线程,并不需要等待追加的任务执行完成。

异步执行+主队列

#pragma mark - 异步执行+主队列
/*
 * 特点:
 * 1.在当前线程(主线程)中执行任务
 * 2.按序执行任务,执行行完一个任务,再执行下一个任务
 * 3.不会阻塞当前线程
 */
- (IBAction)executeAsyncMainQueueTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"AsyncMainQueueTask---begin");
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_async(mainQueue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_async(mainQueue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_async(mainQueue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"AsyncMainQueueTask---end");
    NSLog(@"*********************************************************");
}

执行结果如下:

2019-04-22 18:28:03.990381+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] CurrentThread begin---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:03.990589+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] AsyncMainQueueTask---begin
2019-04-22 18:28:03.990808+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] CurrentThread end---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:03.990959+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] AsyncMainQueueTask---end
2019-04-22 18:28:03.991088+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] *********************************************************
2019-04-22 18:28:05.993620+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 1---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:07.994036+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 1---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:09.995547+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 2---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:11.997136+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 2---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:13.997717+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 3---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:15.998158+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 3---<NSThread: 0x600003ee6900>{number = 1, name = main}

通过我们的代码测验,可以看出:

  1. 3个任务在主线程中,按序执行

  2. 从我们的log中可以看到,begin的log之后,马上就是end的log,因此可以看出,它并不会阻塞当前线程,并不需要等待追加的任务执行完成。

同步执行+主队列

#pragma mark - 同步执行+主队列
/*
 * 特点:
 * 会直接产生死锁
 */
- (IBAction)executeSyncMainQueueTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"SyncMainQueueTask---begin");
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_sync(mainQueue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_sync(mainQueue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    dispatch_sync(mainQueue, ^{
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"SyncMainQueueTask---end");
    NSLog(@"*********************************************************");
}

执行结果如下:

2019-04-22 16:04:57.238644+0800 GCD(一) 队列、任务、串行、并发[10673:2238846] CurrentThread begin---<NSThread: 0x6000038ca800>{number = 1, name = main}
2019-04-22 16:04:57.238915+0800 GCD(一) 队列、任务、串行、并发[10673:2238846] SyncMainQueueTask---begin
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    frame #0: 0x000000010d45fa19 libdispatch.dylib`__DISPATCH_WAIT_FOR_QUEUE__ + 444
    frame #1: 0x00007ffee542deb0
    frame #2: 0x000000010d45f3f0 libdispatch.dylib`_dispatch_sync_f_slow + 231
  * frame #3: 0x000000010a7cff5a GCD(一) 队列、任务、串行、并发`-[ViewController executeSyncMainQueueTask:](self=0x00007fb8f25124f0, _cmd="executeSyncMainQueueTask:", sender=0x00007fb8f2515fa0) at ViewController.m:220
    frame #4: 0x000000010e9b9ecb UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
    frame #5: 0x000000010e3f50bd UIKitCore`-[UIControl sendAction:to:forEvent:] + 67
    frame #6: 0x000000010e3f53da UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 450
    frame #7: 0x000000010e3f431e UIKitCore`-[UIControl touchesEnded:withEvent:] + 583
    frame #8: 0x000000010e9f50a4 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2729
    frame #9: 0x000000010e9f67a0 UIKitCore`-[UIWindow sendEvent:] + 4080
    frame #10: 0x000000010e9d4394 UIKitCore`-[UIApplication sendEvent:] + 352
    frame #11: 0x000000010eaa95a9 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3054
    frame #12: 0x000000010eaac1cb UIKitCore`__handleEventQueueInternal + 5948
    frame #13: 0x000000010bab8721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #14: 0x000000010bab7f93 CoreFoundation`__CFRunLoopDoSources0 + 243
    frame #15: 0x000000010bab263f CoreFoundation`__CFRunLoopRun + 1263
    frame #16: 0x000000010bab1e11 CoreFoundation`CFRunLoopRunSpecific + 625
    frame #17: 0x00000001141491dd GraphicsServices`GSEventRunModal + 62
    frame #18: 0x000000010e9b881d UIKitCore`UIApplicationMain + 140
    frame #19: 0x000000010a7d03c0 GCD(一) 队列、任务、串行、并发`main(argc=1, argv=0x00007ffee542ff90) at main.m:14
    frame #20: 0x000000010d4c7575 libdyld.dylib`start + 1
(lldb) 

通过我们的代码测验,可以看出:

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    frame #0: 0x000000010d45fa19 libdispatch.dylib`__DISPATCH_WAIT_FOR_QUEUE__ + 444

应用在主线程同步执行第一个任务时,就会直接crash,我们同步LLDBbt指定查看函数调用栈,可以发现,在系统库libdispatch调用__DISPATCH_WAIT_FOR_QUEUE__函数时,就会产生一个由队列引起的循环等待导致的crash,这就是我们常说的Deadlock死锁,接下里我们来详细介绍一下死锁产生的原因与注意事项。

GCD中产生死锁的原因

- (IBAction)executeSyncMainQueueTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"SyncMainQueueTask---begin");
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_sync(mainQueue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
    });

从上面的代码我们可以看出,当我们点击按钮,调用executeSyncMainQueueTask方法时,这时我们其实是在主队列(串行队列)提交了一个任务,我们暂先称它为任务0然后我们又使用dispatch_sync同步执行方法往主队列中提交了任务1Block,现在我们来分析一下,为什么这种情况下会产生死锁

  1. 先往主队列(串行队列)中提交了任务0,然后在任务0执行的过程中同步地往主队列中添加了任务1
  2. 主队列中添加的任务都会在主线程中执行,同时按照串行队列的特点(任务按序执行),主线程中首先会执行任务0,任务0执行完成之后才会去执行任务1,但是在任务0执行的过程中,使用同步方式往主队列中添加任务1,由于是使用同步方式,这时主线程会被阻塞,需要任务1完成之后,任务0才会继续往下执行。由此,我们可以看出,由于串行队列的特性,任务1会依赖于任务0的执行完成才会继续往下执行,同时由于同步添加任务的特性(会阻塞当前线程,直到添加的任务执行完成),任务0会依赖于任务1的执行完成。所以,2个任务的执行就会因为相互等待对方的完成,而导致死锁。

通过上面的分析,我们可以看出这里产生死锁的一个很重要的原因就是主队列是一个串行的队列(主队列中只有一条主线程)。如果我们如下例,在并发队列中提交,则不会造成死锁:

dispatch_async(dispatch_get_global_queue(0, 0), ^{ //这里是为了保证当前任务是处于并发队列开辟的线程中,而不是主线程中
  dispatch_sync(dispatch_get_global_queue(0, 0), ^{
      NSLog(@"任务0");
  });
  NSLog(@"任务1");
});

原因是并发队列中的任务执行时并行的,所以,任务1并不会一直等待任务0执行完成,才去执行,而是直接执行完。因此任务0因为任务1的结束,线程阻塞也会被消除,任务0得以继续执行。

我们再开看一组示例:

//目前处于主线程中
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
      NSLog(@"任务0");
});
NSLog(@"任务1");

我们在主线程中,往全局队列同步提交了Block,因为全局队列和主队列是两个队列,所以任务1的执行,并不需要等待任务0。所以等任务0结束,任务1也可以被执行。
当然这里因为提交Block所在队列,Block被执行的队列是完全不同的两个队列,所以这里用串行queue,也是不会死锁的,到这里我们也可以知道一些同步提交(dispatch_sync)的阻塞机制:

同步提交Block,首先是阻塞的当前提交Block的线程,而在队列中,同步提交的Block,只会阻塞串行队列(由串行队列的同一时间只能执行一个任务的特性决定),并不会阻塞并发队列,当然dispatch_barrier系列的除外,这个我会在后面的文章中讲到,欢迎大家继续关注我的博客

现在我们可以用一句话来总结产生死锁的原因就是:

使用同步方式(dispatch_sync)提交一个任务到一个串行队列时,如果提交这个任务的操作所处的线程,也是处于这个串行队列,就会引起死锁

开发中如何避免产生死锁

6种组合使用总结

总结 串行队列 并发队列 主队列
同步添加(sync) 不开辟新线程,在当前线程中串行执行任务 不开辟新线程,在当前线程中串行执行任务 死锁
异步添加(async) 开辟新线程(1条),串行执行任务 开辟新线程(1/n条),并发执行任务 不开辟新线程,在主线程中顺序执行

线程间通信

#pragma mark - 子线程执行耗时代码,主线程更新UI
- (IBAction)threadInteraction:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"threadInteraction---begin");
    
    //异步添加任务到全局并发队列执行耗时操作
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        //执行耗时任务
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        }
        
        //回到主线程更新UI
        dispatch_sync(dispatch_get_main_queue(), ^{
            
            //Do something here to update UI
            
        });
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"threadInteraction---end");
    NSLog(@"*********************************************************");
}

执行结果如下:

2019-04-22 18:47:54.836027+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] CurrentThread begin---<NSThread: 0x6000038e9400>{number = 1, name = main}
2019-04-22 18:47:54.836217+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] threadInteraction---begin
2019-04-22 18:47:54.836436+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] CurrentThread end---<NSThread: 0x6000038e9400>{number = 1, name = main}
2019-04-22 18:47:54.836619+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] threadInteraction---end
2019-04-22 18:47:54.836761+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] *********************************************************
2019-04-22 18:47:56.839416+0800 GCD(一) 队列、任务、串行、并发[13229:2669427] 1---<NSThread: 0x6000038b6d40>{number = 4, name = (null)}
2019-04-22 18:47:58.840646+0800 GCD(一) 队列、任务、串行、并发[13229:2669427] 1---<NSThread: 0x6000038b6d40>{number = 4, name = (null)}

在平常的开发中,我们最常用的就是提交一个任务到全局并发队列取执行一些比较耗时的操作(比如,文件下载、文件上传、图片解码、数据库操作、IO读写),然后再切回到主线程去更新UI,苹果官方限定了更新UI的操作只能在主线程中执行,所以,我们最后还是要回到主线程去处理我们的UI交互。

本文到这里已经基本结束,在接下来的文章中,我将会继续讲解GCD多线程编程的另外几个知识点,也是我们平时开发中实际很经常会用到的,如dispatch_barrierdispatch_groupdispatch_semaphore线程安全的相关内容

如果文中有错误的地方,或者与你的想法相悖的地方,请在评论区告知我,我会继续改进,如果你觉得这个篇文章总结的还不错,麻烦动动小手,给我的文章与Git代码样例点个✨

上一篇下一篇

猜你喜欢

热点阅读