GCD ( 一 ) :多线程,任务,队列,同步异步

一、简介:

GCD(Grand Central Dispatch)是苹果推出的一种多线程技术,在iOS4及以上版本使用.
好处:
1.利用多核CPU的优势,进行并行计算
2.自动管理线程的生命周期,线程的创建、销毁以及任务调度,不需要编写管理线程的代码

几个需要掌握并深刻理解的概念:
线程:一条执行任务的通道.

任务:就是你要执行的代码片段.

同步与异步:任务的执行方式,主要决定的是能不能开启新线程.

同步表示在当前线程中执行任务,异步表示在新的线程中执行任务,不阻塞当前线程,同步不具备开启新线程的能力,异步具备开新线程的能力,但是,不一定真的开启新线程,由队列的类型决定.

队列:(Queues)存放待执行任务的排队队列.任务的执行有队列进行分发.

1.队列管理你提供给 GCD 的任务并用 FIFO 顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。
2.所有的调度队列(dispatch queues)自身都是线程安全的,你能从多个线程并行的访问它们。当你了解了调度队列如何为你自己代码的不同部分提供线程安全后,GCD的优点就是显而易见的。关键是选择正确类型的调度队列和正确的调度函数来执行你的任务。
3.GCD提供两种调度队列,串行队列,并发队列(不是并行队列)

串行队列:串行队列中的任务是一个接一个执行one by one,每个任务只在前一个任务完成时才开始。

这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。

由于在串行队列中不会有两个任务并发运行,因此不会出现同时访问临界区的风险;相对于这些任务来说,这就从竞态条件下保护了临界区。所以如果访问临界区的唯一方式是通过提交到调度队列的任务,那么你就不需要担心临界区的安全问题了

并发队列在并发队列中的任务能得到的保证是它们会按照被添加的顺序(FIFO)开始执行。但任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。这完全取决于 GCD 。

并发与并行:并发和并行通常被一起提到,所以值得花些时间解释它们之间的区别。

并发是队列中任务可以执行在多条通道中,具体由系统派发.多核设备通过并行来同时执行多个线程,单核设备也能实现这一点,它们必须先运行一个线程,执行一个上下文切换,然后运行另一个线程或进程.
并行是指你的多个工作片段,同时在执行.例如,你需要下载三张图片,那么三张图片分别在下载并且进度独立,就是并行.

虽然你可以编写代码在 GCD 下并发执行,但 GCD 会决定有多少并行的需求。并行要求并发,但并发并不能保证并行

二、基本用法:

创建队列:这里使用 dispatch_queue_create 初始化一个队列。第一个参数是反向DNS样式命名惯例;确保它是描述性的,将有助于调试。第二个参数指定你的队列是串行还是并发。

1.串行队列:

//创建串行队列:  
dispatch_queue_t queue = dispatch_queue_create("com.Test.queue", DISPATCH_QUEUE_SERIAL);

注意:你会经常看人们传递 0 或者 NULL 给 dispatch_queue_create 的第二个参数。这是一个创建串行队列的过时方式;明确你的参数总是更好。

2.并发队列:

// 创建并发队列:
dispatch_queue_t queue = dispatch_queue_create("com.Test.queue", DISPATCH_QUEUE_CONCURRENT);

3.获取主队列

 // 获取主队列(主队列是串行队列)
    dispatch_queue_t queue = dispatch_get_main_queue();

主队列(main queue) 是由系统提供给你一个特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发生消息给 UIView 或发送通知的。

4.获取全局并发队列
GCD提供了一个全局的并发队列,使用dispatch_get_global_queue获取,需要传入两个参数,第一个参数是队列的优先级,第二个是预留参数,传0即可

 // 获取全局并发队列
 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

全局队列有着不同的优先级:background、low、default 以及 high。要知道,Apple 的 API 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务。

创建执行任务的方法:
1.同步任务:

   dispatch_sync(queue, ^{
        // 同步任务执行的代码
    });

2.异步任务:

   dispatch_async(queue, ^{
        // 异步任务执行的代码
    });

编写GCD代码:编写GCD代码要指明任务执行的方式(同步还是异步),任务执行的线程.因此,会有如下几种情况:
1.串行队列,同步任务
2.串行队列,异步任务
2.并发队列,同步任务
4.并发队列,异步任务
5.主队列,同步任务
6.主队列,异步任务
7.全局并发队列,同步任务
8.全局并发队列,异步任务

不管我们选择以何种方式编写代码,我们的初衷是能清楚的知道自己的代码片段是如何在线程中被分发被执行的(清楚自己代码片段的耗时时间的前提下),我们想明确几个指导原则:
1.同步任务是不会开启线程的;
2.异步任务具备开启线程的能力,但要看队列是否是主队列,线程的具体开辟由GCD决定;
3.主队列dispatch_get_main_queue是串行的,队列中的任务,在主线程中执行;

  1. dispatch_get_global_queue是全局并发队列,具有四种优先级别;

我们先验证上面几种组合的执行情况:

1.串行队列,同步任务

    dispatch_queue_t serialQueue = dispatch_queue_create("com.testQueue.serial", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        [self timeMethod:@"串行队列--同步任务1"];
    });
    dispatch_sync(serialQueue, ^{
        [self timeMethod:@"串行队列--同步任务2"];
    });
//    2019-05-15 17:04:06.805232+0800 GCDDemo[61363:3115102] 串行队列--同步任务1---<NSThread: 0x6000020808c0>{number = 1, name = main}
//    2019-05-15 17:04:07.806662+0800 GCDDemo[61363:3115102] 串行队列--同步任务1---<NSThread: 0x6000020808c0>{number = 1, name = main}
//    2019-05-15 17:04:08.807174+0800 GCDDemo[61363:3115102] 串行队列--同步任务2---<NSThread: 0x6000020808c0>{number = 1, name = main}
//    2019-05-15 17:04:09.808687+0800 GCDDemo[61363:3115102] 串行队列--同步任务2---<NSThread: 0x6000020808c0>{number = 1, name = main}

上面代码的执行过程:
1.同步任务,不会开辟线程,运行在当前线程;
2.当前线程为主线程;
3.串行队列,所以任务是顺序执行的
结论:没有开启新线程 ,串行执行,主线程中执行

2.串行队列,异步任务

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

    dispatch_async(serialQueue, ^{
        [self timeMethod:@"串行队列--异步任务1"];
    });

    dispatch_async(serialQueue, ^{
        [self timeMethod:@"串行队列--异步任务2"];
    });
    
//    2019-05-15 17:13:08.344125+0800 GCDDemo[61652:3122985] 串行队列--异步任务1---<NSThread: 0x6000014cc2c0>{number = 3, name = (null)}
//    2019-05-15 17:13:10.347402+0800 GCDDemo[61652:3122985] 串行队列--异步任务1---<NSThread: 0x6000014cc2c0>{number = 3, name = (null)}
//    2019-05-15 17:13:12.347897+0800 GCDDemo[61652:3122985] 串行队列--异步任务2---<NSThread: 0x6000014cc2c0>{number = 3, name = (null)}
//    2019-05-15 17:13:14.353511+0800 GCDDemo[61652:3122985] 串行队列--异步任务2---<NSThread: 0x6000014cc2c0>{number = 3, name = (null)}

上面代码的执行过程:
1.异步任务,具备开辟线程能力;
3.串行队列,所以任务是顺序执行的
结果:开启新线程,串行执行

3.并发队列,同步任务

    dispatch_queue_t concunrrentQueue = dispatch_queue_create("com.testQueue.CONCURRENT", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(concunrrentQueue, ^{
        [self timeMethod:@"并发队列--同步任务1"];
    });
    dispatch_sync(concunrrentQueue, ^{
        [self timeMethod:@"并发队列--同步任务2"];
    });

//    2019-05-15 17:15:33.557345+0800 GCDDemo[61750:3125543] 并发队列--同步任务1---<NSThread: 0x6000007fd400>{number = 1, name = main}
//    2019-05-15 17:15:35.558439+0800 GCDDemo[61750:3125543] 并发队列--同步任务1---<NSThread: 0x6000007fd400>{number = 1, name = main}
//    2019-05-15 17:15:37.559989+0800 GCDDemo[61750:3125543] 并发队列--同步任务2---<NSThread: 0x6000007fd400>{number = 1, name = main}
//    2019-05-15 17:15:39.561567+0800 GCDDemo[61750:3125543] 并发队列--同步任务2---<NSThread: 0x6000007fd400>{number = 1, name = main}

上面代码的执行过程:
1.同步任务,不会开辟线程,运行在当前线程;
2.当前线程为主线程;
3.并发队列,但由于任务是同步的,所以任务是顺序执行的
结果:没有开启新线程 ,串行执行,主线程执行

4.并发队列,异步任务

    dispatch_queue_t concunrrentQueue = dispatch_queue_create("com.testQueue.CONCURRENT", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(concunrrentQueue, ^{
        [self timeMethod:@"并发队列--异步任务1"];
    });
    dispatch_async(concunrrentQueue, ^{
        [self timeMethod:@"并发队列--异步任务2"];
    });
    
//    2019-05-15 17:14:39.933850+0800 GCDDemo[61719:3124710] 并发队列--异步任务1---<NSThread: 0x600000e56800>{number = 3, name = (null)}
//    2019-05-15 17:14:39.933849+0800 GCDDemo[61719:3124711] 并发队列--异步任务2---<NSThread: 0x600000e56a80>{number = 4, name = (null)}
//    2019-05-15 17:14:41.936166+0800 GCDDemo[61719:3124711] 并发队列--异步任务2---<NSThread: 0x600000e56a80>{number = 4, name = (null)}
//    2019-05-15 17:14:41.936166+0800 GCDDemo[61719:3124710] 并发队列--异步任务1---<NSThread: 0x600000e56800>{number = 3, name = (null)}

上面代码的执行过程:
1.异步任务,具备开启线程能力;
2.并发队列,异步任务,并发执行
3.线程开辟情况由GCD确定,任务执行顺序由任务本身耗时情况决定.
结果:开启新线程,并发执行

5.主线程同步任务

- (void)sysMain{
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"---%@",[NSThread currentThread]);
    });
}

上面代码的执行过程:
1.主队列是串行队列,而且在主线程;
2.添加同步任务,同步任务需要等待主线程中的任务全部执行完再执行当前任务,
结果:发生死锁

6.主队列,异步任务

    dispatch_async(dispatch_get_main_queue(), ^{
        [self timeMethod:@"主队列--异步任务1"];
    });

    dispatch_async(dispatch_get_main_queue(), ^{
        [self timeMethod:@"主队列--异步任务2"];
    });
    
//    2019-05-15 17:19:15.305626+0800 GCDDemo[61873:3128986] 主队列--异步任务1---<NSThread: 0x600002fa6940>{number = 1, name = main}
//    2019-05-15 17:19:17.307348+0800 GCDDemo[61873:3128986] 主队列--异步任务1---<NSThread: 0x600002fa6940>{number = 1, name = main}
//    2019-05-15 17:19:19.308964+0800 GCDDemo[61873:3128986] 主队列--异步任务2---<NSThread: 0x600002fa6940>{number = 1, name = main}
//    2019-05-15 17:19:21.310305+0800 GCDDemo[61873:3128986] 主队列--异步任务2---<NSThread: 0x600002fa6940>{number = 1, name = main}

上面代码的执行过程:
1.异步任务,有开辟线程的能力;
2.主队列在主线程中运行,所以不开启新线程;
3.主队列为串行队列,所以任务串行执行.
结果:没有开启新线程,串行执行

7.全局并发队列,同步任务

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self timeMethod:@"全局并发队列--同步任务1"];
    });

    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self timeMethod:@"全局并发队列--同步任务2"];
    });
    
//    2019-05-15 17:21:56.781514+0800 GCDDemo[61953:3131089] 全局并发队列--同步任务1---<NSThread: 0x600002be9300>{number = 1, name = main}
//    2019-05-15 17:21:58.783111+0800 GCDDemo[61953:3131089] 全局并发队列--同步任务1---<NSThread: 0x600002be9300>{number = 1, name = main}
//    2019-05-15 17:22:00.784664+0800 GCDDemo[61953:3131089] 全局并发队列--同步任务2---<NSThread: 0x600002be9300>{number = 1, name = main}
//    2019-05-15 17:22:02.786188+0800 GCDDemo[61953:3131089] 全局并发队列--同步任务2---<NSThread: 0x600002be9300>{number = 1, name = main}

上面代码的执行过程:
1.同步任务,不开启新线程,在当前线程执行;
2.全局并发队列是并发队列,但任务为同步,所以串行执行'

结果:没有开启新线程 ,串行执行,主线程

8.全局并发队列,异步任务

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self timeMethod:@"全局并发队列--异步任务1"];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self timeMethod:@"全局并发队列--异步任务2"];
    });
    
//    2019-05-15 17:23:37.208830+0800 GCDDemo[62018:3132621] 全局并发队列--异步任务1---<NSThread: 0x600000baad00>{number = 4, name = (null)}
//    2019-05-15 17:23:37.208892+0800 GCDDemo[62018:3132622] 全局并发队列--异步任务2---<NSThread: 0x600000baacc0>{number = 3, name = (null)}
//    2019-05-15 17:23:39.213383+0800 GCDDemo[62018:3132621] 全局并发队列--异步任务1---<NSThread: 0x600000baad00>{number = 4, name = (null)}
//    2019-05-15 17:23:39.213384+0800 GCDDemo[62018:3132622] 全局并发队列--异步任务2---<NSThread: 0x600000baacc0>{number = 3, name = (null)}

上面代码的执行过程:
1.异步任务,有开启线程能力;
2.全局并发队列,开启新线程,并发执行
结果:开启新线程,并发执行

运行结果统计在下表:

区别 并发队列 串行队列 主队列 全局并发队列
同步(sync) 没有开启新线程 ,串行执行 没有开启新线程,串行执行 主线程调用,发生死锁 没有开启新线程 ,串行执行,主线程
异步(async) 有开启新线程,并发执行 有开启新线程,串行执行 没有开启新线程,串行执行 开启新线程,并发执行

从上表可以得出结论:
1.同步(sync),不具备开启线程的条件;
2.异步(async),具备开启线程的条件,但并不一定开启新线程,这跟任务指定的队列类型有关.
3.主线程同步会发生死锁.
4.主队列是串行队列,但不同于自定义的串行队列.
5.全局并发队列是并发队列,同自定义并发队列情况一致.
6.并发队列,只有在异步时才会真正开启线程并发执行.

至此,大致的组合情况已经清楚了,但是还有一些情况需要验证,
比如:
1.在子线程中执行主队列同步任务会怎样?是否会死锁?

- (void)subThreadSyncMain{
    
    [NSThread detachNewThreadWithBlock:^{
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"1---%@",[NSThread currentThread]);
        });
        
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"2---%@",[NSThread currentThread]);
        });
    }];

//    2019-05-16 14:22:47.080669+0800 GCDDemo[97183:4202776] 1---<NSThread: 0x60000239d380>{number = 1, name = main}
//    2019-05-16 14:22:47.081528+0800 GCDDemo[97183:4202776] 2---<NSThread: 0x60000239d380>{number = 1, name = main}

}
- (void)subThreadAsyncMain{
    
    [NSThread detachNewThreadWithBlock:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"1---%@",[NSThread currentThread]);
        });
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"2---%@",[NSThread currentThread]);
        });
    }];
//    2019-05-16 14:23:46.261385+0800 GCDDemo[97249:4208648] 1---<NSThread: 0x600000941440>{number = 1, name = main}
//    2019-05-16 14:23:46.261564+0800 GCDDemo[97249:4208648] 2---<NSThread: 0x600000941440>{number = 1, name = main}
}

结果:没有发生死锁,任务顺序执行,并且在主线程中执行.原因是主队列中的任务都在主线程中执行

思考一下下面的执行顺序是怎样的?

- (void)subThreadSysSerianl{
    
    dispatch_queue_t queue = dispatch_queue_create("com.test", DISPATCH_QUEUE_SERIAL);// 1
    [NSThread detachNewThreadWithBlock:^{//2
        dispatch_sync(queue, ^{//3
            NSLog(@"1");
        });
        dispatch_sync(queue, ^{//4
            NSLog(@"2");
        });
    }];
}

答案是: 1,2
上面代码的执行过程:
1.创建一个串行队列;
2.开启一个子线程;
3.在当前队列中添加任务1;
4.在当前队列中添加任务2;
由于是串行队列同步任务,队列按照FIFO的顺序执行,所以任务1和任务2顺序执行.
那么将任务二放在任务一中,会发生什么?

- (void)subThreadSysSerianl{
    dispatch_queue_t queue = dispatch_queue_create("com.test", DISPATCH_QUEUE_SERIAL);// 1
    [NSThread detachNewThreadWithBlock:^{//2
        dispatch_sync(queue, ^{//3
            NSLog(@"1---%@",[NSThread currentThread]);
            dispatch_sync(queue, ^{//4
                NSLog(@"2---%@",[NSThread currentThread]);
            });
        });
    }];
}

会发生死锁:
我们来捋顺一下执行过程:
1.前提是串行队列同步任务,这很重要;
2.然后先将任务1放进队列中;
3.任务1先执行一步打印输出1---;
4.然后执行的是异步任务2;
5.根据串行队列同步任务的特点,执行到任务2时,任务2要等待任务1执行完成才可以执行,而任务1又在等待任务2执行完成.因此发生死锁.
6.对比上面发生的主队列同步任务发生死锁,就更好理解了.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容