GCD技术

初识GCD

在我们平时的OC开发中经常需要用到多线程编程,而GCD这项技术是最受开发者喜爱的多线程技术,我们今天就一起来学习这门技术.

GCD可以说是一项划时代的技术,它用非常简单的语法,实现了非常复杂的多线程编程.

使用GCD,我们只需要把需要执行的任务追加到合适的队列Dispatch Queue中,GCD就能够生成适当的线程去执行任务

而GCD是一项非常底层的技术,苹果公司为我们做了大量的优化,所以我们才能如此优雅的进行多线程编程

GCD中最重要的概念一:队列

在GCD中大致可以分为两种队列:Serial Dispatch Queue(串行队列)Concurrent Dispatch Queue(并行队列)

苹果对GCD技术的使用指南上告诉我们:我们只需要把需要执行的任务追加到合适的队列中,GCD就会自动为我们生成并分配适当的线程去执行任务

既然是这样,那我们对于这两种队列特点的理解,就尤为重要了,只有选对追加的队列,我们才能达到想要的效果.

  • Serial Dispatch Queue(串行队列):队列中的任务会按顺序分配到线程并执行,但是只有上一个任务执行完毕后,才会分配下一个任务并执行,所以串行队列同一时间只能执行一个任务,所以同一时间也只需要一个线程,任务会一个一个按顺序的执行完毕.具体某个具体任务的线程是当前线程还是GCD为我们创建的其它线程,则要取决于该任务的调用方式,是同步调用,还是异步调用

  • Concurrent Dispatch Queue(并行队列):并行队列,队列中的任务也会按顺序分配执行,但是与串行队列不同的是,不需要等待当前任务执行完毕,才分配下一个任务,可以在当前任务执行时分配下一个任务(如果下一个任务可以分配到与当前任务不同的线程而且该线程空闲),直到可利用的线程都有正在执行的任务,便会等待某一线程执行完毕,继续给该线程分配任务,但是当分配到不同线程同时执行时,任务执行完的顺序就不可控了,执行完的顺序一般不会和追加任务到队列的顺序一致,适用于不关心执行顺序的任务.如果该任务要求使用不同于当前线程的线程执行,那么并行队列可同时利用起多条线程,分配子线程的数量和分配给哪些子线程,是由GCD底层根据系统状态,CPU型号等等因素决定的,GCD会帮助我们开启恰到好处的线程数量,开发者不需要关心.

上面的这两种队列是我们GCD中的两种队列类型,这两种队列都可以使用相关函数进行创建;

//创建一个串行队列
dispatch_queue_t serialQueue= dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
//创建一个并行队列
dispatch_queue_t conCurrentQueue= dispatch_queue_create("queueName", DISPATCH_QUEUE_CONCURRENT);

系统提供的几种特殊队列:

Main Dispatch Queue(系统主队列):这个队列是系统创建的一个特殊的串行队列,说它特殊,是因为它队列里的任务只能在系统主线程执行,无论是同步调用还是异步调用这个队列中的任务,也不会改变它是在主线程里执行的事实.值得注意的是如果在主线程下在主队列中追加同步任务,会导致死锁,至于死锁的原因,我们下面对同步调用这种方式进行研究的时候再讨论.这个队列也是我们平时编码是默认的队列,我们的UI操作通常都在这个队列中

Global Dispatch Queue(系统全局队列):这个队列是系统生成提供给我们的一个并发队列,这个队列我们可以很方便的拿来使用,省去自己创建并发队列的繁琐.而且全局队列有四个优先级,其实对应的是四个全局队列,优先级分别是:high,default,low,background,我们应该把需要追加到队列里的任务追加到合适的全局队列中.如果我们创建的队列想要设定优先级,一般也会以这四个全局队列为参考,设置该队列与某全局队列优先级一致,具体函数,我们再下文中继续讨论.

GCD中最重要的概念二:任务的同步异步

首先,GCD中同步和异步的概念是相对于任务而言的,并不是对队列而言的,大家可以理解为任务的一个属性,开发者把某个任务追加到队列中时,会声明这个任务调用的方式,一个队列中即可以有同步调用的任务,也可以有异步调用的任务,他们都会按照顺序被分配,只是根据每个任务声明的调用方式不同,分配到不同的线程去执行.我们来看一看具体的区别吧.

异步调用:追加异步调用的任务到某个队列中.意思是当队列分配这个任务时,这个任务需要在非当前线程执行也就是子线程执行(主队列中的任务除外,主队列中的所有任务都只能在主线程执行,无论同步调用还是异步调用),当串行队列分配异步任务时,由于需要等到当前任务执行完毕时再执行下一任务,所以无论串行队列中有多少异步任务,也额外只需要一条子线程就可以,该子线程执行完毕当前任务,再执行下一个任务.速度比较慢.当并行队列分配异步任务时因为不需要等待当前任务执行完,所以可以在当前任务执行时,分配下一个异步任务到其它子线程,直到所有可利用的子线程都被分配了任务且正在执行任务,当一旦有这些子线程空闲,并行队列就会继续往这些空闲的队列中分配下一个任务.很明显如果有大量异步任务,则追加到并行队列处理速度更快,但是执行完毕的顺序却不可控,追加到串行队列的异步任务速度虽慢一些顺序却完全可控,大家可根据具体业务情况去选择.

我们看一下GCD中追加一个异步任务的API:

    dispatch_async(conCurrentQueue, ^{
        
        for (int i=0; i<10000; i++){
            NSLog(@"do some thing%@",[NSThread currentThread]);
        }
        
    });
    
    NSLog(@"do other thing%@",[NSThread currentThread]);

这个API除了把所要执行的该任务标记为异步调用追加到相应队列外,它还有如下特性:
它只负责追加任务到队列,并不关心该任务是否执行,追加完后就会继续执行接下来的代码,至于任务执行则让队列自行安排.

所以我们控制台的打印会如下:

2017-04-08 15:30:04.886 GCDTest[1498:690881] do some thing<NSThread: 0x600000264640>{number = 3, name = (null)}
2017-04-08 15:30:04.886 GCDTest[1498:690846] do other thing<NSThread: 0x600000076a00>{number = 1, name = main}
2017-04-08 15:30:04.887 GCDTest[1498:690881] do some thing<NSThread: 0x600000264640>{number = 3, name = (null)}
2017-04-08 15:30:04.887 GCDTest[1498:690881] do some thing<NSThread: 0x600000264640>{number = 3, name = (null)}
2017-04-08 15:30:04.887 GCDTest[1498:690881] do some thing<NSThread: 0x600000264640>{number = 3, name = (null)}
2017-04-08 15:30:04.887 GCDTest[1498:690881] do some thing<NSThread: 0x600000264640>{number = 3, name = (null)}
2017-04-08 15:30:04.888 GCDTest[1498:690881] do some thing<NSThread: 0x600000264640>{number = 3, name = (null)}
2017-04-08 15:30:04.888 GCDTest[1498:690881] do some thing<NSThread: 0x600000264640>{number = 3, name = (null)}
...

我们可以看到,在第二行就输出了do other thing并没有等到加载到队列中的任务执行完毕才继续往下执行.

同步调用:当该任务时同步调用时,分配该任务的队列会将任务分配到当前线程执行,所以无论是串行队列还是并行队列,都得等到当前线程执行的任务执行完之后才能分配队列中的下一个任务,所以看起来都是在当前线程一个一个执行的,但是主队列的任务还是只会在主线程执行.

我们看一下向队列中追加一个同步任务的API:

    dispatch_sync(conCurrentQueue, ^{
        
        for (int i=0; i<10000; i++){
            NSLog(@"do some thing%@",[NSThread currentThread]);
        }
        
    });
    
    NSLog(@"do other thing%@",[NSThread currentThread]);

在语法上与追加异步任务只是方法名不一样,而在效果上,这段代码和异步追加任务的结果大不一样,以下是控制台的打印:

...
2017-04-08 17:29:12.454 GCDTest[1745:969597] do some thing<NSThread: 0x608000263b00>{number = 1, name = main}
2017-04-08 17:29:12.454 GCDTest[1745:969597] do some thing<NSThread: 0x608000263b00>{number = 1, name = main}
2017-04-08 17:29:12.454 GCDTest[1745:969597] do some thing<NSThread: 0x608000263b00>{number = 1, name = main}
2017-04-08 17:29:12.455 GCDTest[1745:969597] do some thing<NSThread: 0x608000263b00>{number = 1, name = main}
2017-04-08 17:29:12.455 GCDTest[1745:969597] do some thing<NSThread: 0x608000263b00>{number = 1, name = main}
2017-04-08 17:29:12.455 GCDTest[1745:969597] do some thing<NSThread: 0x608000263b00>{number = 1, name = main}
2017-04-08 17:29:12.455 GCDTest[1745:969597] do some thing<NSThread: 0x608000263b00>{number = 1, name = main}
2017-04-08 17:29:12.456 GCDTest[1745:969597] do some thing<NSThread: 0x608000263b00>{number = 1, name = main}
2017-04-08 17:29:12.456 GCDTest[1745:969597] do other thing<NSThread: 0x608000263b00>{number = 1, name = main}

大家可以看到,当所有同步追加到队列中的任务执行完毕后才会执行接下来的代码,在最后才输出了do other thing,这便是这两个API除了追加任务的属性不同外最大的区别,这个API会一直等到追加过的任务执行结束.

dispatch_sync意义在哪里

有的同学看到这里就会有个巨大的疑问,dispatch_sync同步调用的任务,并不会开启子线程在子线程执行,那这种方式有什么意义呢?为什么不直接写任务代码就好了,为什么还要放到队列里,同步调用?

有的时候我们会需要保证任务执行的次序,比如,我们有n个任务需要执行,这n个任务中,既有同步调用的任务,也有异步调用的任务,现在有第n+1个任务需要在当前线程执行,而且要保证前面的n个任务已经执行完毕,那么这个时候,就需要我们将这个第n+1个任务放到一个串行队列的尾部,来保证它在最后运行.

dispatch_sync的等待会阻塞当前线程,这是我们大家要注意的.如果阻塞主线程会引起UI交互的卡顿.如果是在子线程,则不会有对交互有影响,只是它后面的代码需要等待它执行完毕.

下面的两段代码就是dispatch_sync和直接代码的对比,大家运行一下,就可以看出差别.
使用dispatch_sync:


for (int i=0; i<10; i++) {
        
        dispatch_async(serialQueue, ^{
            sleep(1.0);
            NSLog(@"do some thing%@",[NSThread currentThread]);
            
        });
    }
    
    dispatch_sync(serialQueue, ^{
        
        NSLog(@"do other thing%@",[NSThread currentThread]);
    });

不使用dispatch_sync:


for (int i=0; i<10; i++) {
        
        dispatch_async(serialQueue, ^{
            sleep(1.0);
            NSLog(@"do some thing%@",[NSThread currentThread]);
            
        });
    }
    
    NSLog(@"do other thing%@",[NSThread currentThread]);

dispatch_sync串行队列死锁问题

在我们追加同步任务到串行队列中时,如果使用不当,将有可能导致死锁的问题.为了避免这种情况的发生,我们要先来分析一下产生死锁的原因.

产生死锁的情况:"追加某个同步任务"这个任务,和追加的同步任务都追加在同一条串行队列中

原因分析:我们知道dispatch_sync追加同步任务这个函数将会等待追加的任务执行完毕才会结束,那么执行到这里时将会产生这样的情况:

"追加同步任务"这个任务需要等待追加的同步任务执行完毕后才能结束

而这个同步任务所在的串行队列则需要等待位于队列前面的"追加同步任务"这个任务执行结束,才开始执行这个同步任务

所以这就造成了一个互相等待的局面,将会导致死锁程序崩溃,我们要全力避免这种情况的发生.最容易犯的错误是在主队列中向主队列添加同步任务.这便满足了上述构成死锁的条件,我们要切忌犯此错误.

GCD其它常用API

dispatch_queue_create创建队列

这个函数是创建GCD的队列所用的函数,这个函数可以生成串行队列和并行队列;

//生成串行队列
dispatch_queue_t serialQueue= dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
    
//生成并行队列    
dispatch_queue_t conCurrentQueue= dispatch_queue_create("queueName", DISPATCH_QUEUE_CONCURRENT);

第一个参数用来表示这个队列,相当于队列名字,在用instrument调试或者产生崩溃日志时可以用的到.第二个参数用来表示是串行队列还是并行队列.

dispatch_set_target_queue设定优先级

这个函数有两个功能:

  1. 可以将某个队列的优先级指定为和另一个队列一致

    //制定queue1的优先级与高优先级全局队列一致
    dispatch_set_target_queue(queue1,   dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
    
  2. 将某几个串行队列指定到另一个串行队列,大家可以理解为将几个串行队列合并成了一个串行队列,几个串行队列里的任务将不会同时执行

dispatch_queue_t serialQueue= dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
    
    dispatch_queue_t serialQueue2= dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
    
    dispatch_queue_t serialQueue3= dispatch_queue_create("queueName", DISPATCH_QUEUE_SERIAL);
    
    dispatch_set_target_queue(serialQueue, serialQueue3);
    dispatch_set_target_queue(serialQueue2, serialQueue3);
    
    
    for (int i=0; i<10; i++) {
        
        dispatch_async(serialQueue, ^{
            sleep(1.5);
            NSLog(@"%@",[NSThread currentThread]);
        });
    }
    
    for (int i=0; i<10; i++) {
        
        dispatch_async(serialQueue2, ^{
            sleep(1.5);
            NSLog(@"%@",[NSThread currentThread]);
        });
    }

Dispatch Group

组的概念,如果我们希望在一些异步任务完成后做一些结果处理,通常,我们会使用串行队列.并把处理操作追加到最后,但是如果需要使用并行队列,或者需要多个队列时,那么代码就会异常的复杂,还好GCD为我们提供给我们Group的概念.

dispatch_queue_t conCurrentQueue= dispatch_queue_create("queueName", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_t group=dispatch_group_create();
    
    
    
    for (int i=0; i<100; i++) {
        
        dispatch_group_async(group, conCurrentQueue, ^{
            
            sleep(0.5);
            NSLog(@"concurrent--%@",[NSThread currentThread]);
        });
    }
    
    dispatch_group_notify(group, conCurrentQueue, ^{
        
        NSLog(@"Done");
    });

上面便是Dispatch Group的常见用法

dispatch_barrier_async任务栅栏

再讨论dispatch_barrier_async之前我先给大家描述一个需求,如果我们需要在一个并发队列中追加20个任务,需要在做完前10个任务后,开始做后10个任务之前,有一段中场休息,在中场休息的时候不执行任何任务,而且,开始中场休息时,保证前十个任务已经完成,后十个任务还没有开始做.休息之后再继续完成后面的10个任务.

我们知道根据并发队列的特性,这是控制不了的,即使把这20个任务,和中场休息任务都按照正确顺序追加到并发队列中,但是并发队列控制不了结束执行的顺序,有可能前后10个任务结束顺序都混在了一起,更别说要在中场休息的时候不做其它任务.那么这个时候就需要我们任务栅栏:dispatch_barrier_async利用它来将中场休息任务追加到并发队列中,就可以满足我们的上述需求:

dispatch_queue_t conCurrentQueue=dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i=1; i<=10; i++) {
        
        dispatch_async(conCurrentQueue, ^{
            
            sleep(0.5);
            NSLog(@"我做完了第%d件事情",i);
        });
    }
    
    dispatch_barrier_async(conCurrentQueue, ^{
        sleep(2.0);
        NSLog(@"我在中场休息 我在中场休息 我在中场休息 我在中场休息 我在中场休息 我在中场休息 我在中场休息 我在中场休息 ");
        
    });
    
    
    for (int i=11; i<=20; i++) {
        
        dispatch_async(conCurrentQueue, ^{
            
            sleep(0.5);
            NSLog(@"我做完了第%d件事情",i);
        });
    }

栅栏就可以保证队列中栅栏前的任务全都执行完毕,开始执行栅栏任务,等待栅栏任务执行完毕,再开始执行栅栏任务之后的任务.清晰的把任务分成了三部分.

dispatch_apply多次追加任务到队列

这个函数可以代替我们循环追加任务到队列中,但是它和dispatch_sync的等待执行完毕的特性一致,这个函数会等待,直到利用dispatch_sync追加到队伍中的任务全部执行完毕才会执行函数下面的代码,期间会阻塞当前线程,所以适合在非主线程使用.

dispatch_queue_t conCurrentQueue= dispatch_queue_create("queueName", DISPATCH_QUEUE_CONCURRENT);


    dispatch_apply(10000, conCurrentQueue, ^(size_t index) {
        
        sleep(0.5);
        NSLog(@"我完成了第%zu个任务",index);
        
    });
    
    NSLog(@"done");

dispatch_after(延迟执行)

我们经常会有希望某段代码延迟一定时间执行.

GCD提供给我们相关的函数,让我们可以很轻易的做到延迟执行

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",[NSThread currentThread]);
    }); 

上述代码在3秒后,进行控制台的打印操作.

严格意义上来说,上述代码并不是3秒后执行,而是3秒后加到主队列中.如果主线程中已经被大量耗时任务堆积时,可能并不准确,但是一般来说还是很可靠的

dispatch_once (一次执行)

有时我们希望某一段代码在整个应用程序执行中,只执行一次,那么就用到GCD的dispatch_once这个API了

static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        NSLog(@"only once");
    });

这个API即使是在多线程环境下,也能保证百分之百的安全.所以也被开发者们所喜爱.也经常用这个API来写单例模式.

dispatch_suspend(队列的暂停)和dispatch_resume(队列的继续)

我们有时候希望在某种状况下停止执行队列中的任务.等到某种条件再继续执行.

我们GCD就为我们提供了相应的函数

以下这个demo是在队列处理任务2秒后暂停队列执行任务,5秒后再回复执行

dispatch_queue_t queue=dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
    
    
    for (int i=0; i<100; i++) {
        
        dispatch_async(queue, ^{
            
            sleep(1.0);
            NSLog(@"执行完第%d次任务",i);
        });
    }
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        dispatch_suspend(queue);
    });
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        dispatch_resume(queue);
    });

其它API

GCD还有一些API,如果感兴趣,大家可以自己搜索资料进行学习,笔者这里就暂时不一一展开了,如果有机会我们后续的文章再一起探讨

  • dispatch I/O

  • dispatch Semaphore

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

推荐阅读更多精彩内容