GCD大法全解析

作者: 9fda4b908fad | 来源:发表于2017-03-04 18:35 被阅读272次

    概述

    说到多线程,对于做iOS的来说,基本上是再熟悉不过的了,多线程以极其高效率的执行代码任务的方式,贯穿于我们项目当中的各个模块.而在整个多线程的体系中,GCD可以说是多线程中的中流砥柱,也是我们绕不过去的一个重点话题.

    什么是GCD?

    让我们来看看苹果对其的描述 : Grand Central Dispatch (GCD)是异步执行任务的技术之一,一般将程序应用中的线程管理用的代码在系统级中实现.开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务.由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可以执行任务,这样就比以前的线程更有效率.

    我们先来看一个例子:

     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        dispatch_async(queue, ^{
            
            /**
             执行耗时操作(如下载图片等)
             */
            dispatch_async(dispatch_get_main_queue(), ^{
                
                /**
                 回到主线程执行UI操作
                 */
            });
            
        });
    

    这个例子我们并不陌生,这里引申出在GCD中非常重要,但特别容易让人混淆的两个概念:

    • 1 . 同步函数和异步函数
    • 2 . 串行队列和并行队列

    1 . 同步(sync)函数和异步(async)函数

    首先应该弄清楚的是,同步函数和异步函数是相对于线程来说的,同步函数在提交任务时,会阻塞当前线程,而异步函数提交任务不会阻塞当前线程,这也佐证了sync无法开启新线程,而async可以.

    2 . 串行(serial)队列和并行(concurrent)队列

    串行队列和并行队列是相对与队列来说的,其实更确切一点说是相对任务来说的,不同的队列决定了任务是按顺序依次执行,还是没有顺序任意执行,串行队列会等待当前任务执行完,再执行后面的任务,而并行队列则不会等待,当前任务执行的同时后面的任务也会开始执行,无论是并行队列还是串行队列,都遵循FIFO(先进先出)原则

    所谓任务,就是我们使用GCD时,提交的那个block

    先用一个例子来解释一下串行和并行队列的不同之处:

      dispatch_async(queue, block0);
      dispatch_async(queue, block1);
      dispatch_async(queue, block2);
      dispatch_async(queue, block3);
      dispatch_async(queue, block4);
      dispatch_async(queue, block5);
      dispatch_async(queue, block6);
      dispatch_async(queue, block7);
    

    如果这里的queue是串行队列,因为要等待当前任务执行完,所以先执行block0,接着block1...,依次执行
    如果这里的queue是并行队列,无需等待block0执行完,所以在执行block0同时也会执行block1,block2...

    获取串行队列和并行队列

    一般来说,我们获取队列有两种方式,一种是直接获取系统的,另外一种是自己创建

    • 1 . 系统提供
    // 全局并行队列
    dispatch_get_global_queue()
    //主队列(存在于主线程中特殊的串行队列)
    dispatch_get_main_queue()
    
    • 2 .自己创建
    // 串行队列 :  DISPATCH_QUEUE_SERIAL换成NULL也是一样
    dispatch_queue_create(@"com.queue.serial", DISPATCH_QUEUE_SERIAL);
    //并行队列
    dispatch_queue_create(@"com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
    

    下面我用具体的案例来分析,加深理解(为了方便,假设我们的代码都是在主线程中)

    • 案例一:
        NSLog(@"任务1");
    
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"任务2");
        });
        
        NSLog(@"任务3");
    
    

    输出结果如下

    sample1.png

    分析:首先打印任务1,接着遇到同步函数,阻塞线程,将任务加入到全局队列中,执行任务2,回到主线程中继续执行任务3

    • 案例二:
        NSLog(@"\n");
        NSLog(@"任务1");
    
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            NSLog(@"任务2");
            
            dispatch_sync(dispatch_get_main_queue(), ^{
                
                  NSLog(@"任务3");
            });
            
            NSLog(@"任务4");
        });
        
        NSLog(@"任务5");
    
    

    输出结果如下


    sample2.png

    分析:打印换行符是为了看打印结果,请忽略,首先执行任务1,接着遇到异步函数,不会阻塞当前线程,继续执行,由于async可以开启新线程,也无需等待,所以任务2和任务5是无序的,然后遇到同步函数,阻塞当前线程,回到主线程中执行任务3,之后才能再继续执行任务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");
    

    输出结果如下


    sample3.png

    分析:开始遇到异步函数,不阻塞也不会等待,async开启新线程,所以任务1和任务4是无序的(注意此时主线程已被循环卡死了),接着遇到同步函数,将任务2加入到主线程中,根据FIFO原则,此时加入的任务需要放到循环后面去执行,但是同步函数需要等待任务2执行完才会继续执行任务3,任务2又必须等待循环执行完再执行,所以就相互卡死在这里了,任务5自然不必说,有循环在,永远不会执行它.

    以上分析的实例都在这里可以找到,我分析的有些地方与博主不同,请注意差异.

    3 . GCD死锁(同样假设代码是在主线程中)

    先解释一下什么是GCD死锁:所谓GCD死锁,就是在一个串行队列中,任务A执行要等待任务B执行完才能执行,但是任务B也在等待任务A执行完,两者相互等待,最后导致都不能执行任务的一种场景(上文案例3其实也可以算是一种死锁)

    先看最常见的死锁:

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

    分析:(首先我们要明白的一点是,这个dispatch执行完的标志是任务1打印了,也就是说代码执行到了最后一个花括号这里).我们主线程的主队列中有一个任务,就是上述所有代码,然后接着往主队列中添加任务1,因为主队列是一个特殊的串行队列,所以任务1需要放到上述代码后面去执行,并进入等待,等待上述代码执行完再执行任务1,但是上述代码执行完的标志是任务1打印,所以上述代码在等待任务1执行完,任务1又在等待上述代码执行完.你等我执行,我等你执行,oh,my god.

    这里有个比较有趣的问题,假如代码是如下这样:

      dispatch_queue_t serialQueue = dispatch_queue_create("com.cib.serialQueue", NULL);
        
        dispatch_sync(serialQueue, ^{
           
            NSLog(@"任务任务你快打印啦...");
        });
    
    

    一个同步函数,一个串行队列,会不会造成死锁的现象呢?
    直接看结果吧:


    打印结果.png

    相信有小伙伴对这个结果感到有点疑惑,一起分析一下:
    首先是同步函数,这段代码会放在主线程执行,并等待执行结果,但是之前我们说,主线程中有一个特殊的串行队列,就是主队列,整段代码是放到主队列中执行的,但是我们在这里是把任务加到我们自己创建的串行队列中去执行的,这两个不同的队列各自只执行自己的那个任务,没有你等我,我等你这个逻辑在,参照死锁的概念,就不难理解这个结果了.

    同步函数和自己创建的串行队列也有死锁,就是下面这种情况:

    dispatch_queue_t queue = dispatch_queue_create("com.queue.serial", DISPATCH_QUEUE_SERIAL);
        
        dispatch_async(queue, ^{
            
            dispatch_sync(queue, ^{
    
                NSLog(@"任务1");
            });
       });
    

    分析:首先遇到异步函数,将任务加入到串行队列中,然后遇上同步函数,阻塞当前线程,将任务追加到串行队列后面,串行队列中已经有了一个任务了,就是这段代码:

      ^{
            NSLog(@"%@",[NSThread currentThread]);
            
            dispatch_sync(queue, ^{
                NSLog(@"任务1");
            });
        }
    

    追加的任务是这段代码:

      ^{
               NSLog(@"任务1");
       }
    

    现在阻塞了当前线程,需要等待追加的任务执行完,才能继续执行,追加的任务又在等第一个任务执行完才能执行,第一个任务执行完的标志是追加的任务执行了(也就是打印完任务1),这不又卡死了.所以索性让你们两者在这千丝万缕的关系中,各自互相伤害去吧!

    4 . 常见GCD用法

    • 1 . dispatch after
      经常会遇到这样的情况,我们希望任务在推迟一段时间后再执行,这样的场景dispatch after就非常合适了(当然除此之外你还可以使用performSelector: withObject: afterDelay:)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            NSLog(@"print in coming 3s");
     });
    

    使用比较简单,这里第一个参数是dispatch_time_t类型,创建dispatch_time_t需要指定从什么时候开始,还有时间间隔,第二个参数是队列,需要注意的是dispatch after并不是在指定时间后执行,而是在指定时间后追加到队列中去

    • 2 . dispatch_group
      有这么一种业务需求,需要执行几种任务,然后等待所有任务都执行完之后,再做一个统一的操作.比如说我们需要下载几张图片,等到所有图片都下载完成之后,再拼接在一起组合成一张新的图片.这时候一种可行的方式是把任务加到一个串行队列中去,让下载操作一个一个进行,目的是可以达到的,但是就体验和效率方面来讲,是不可取的,但是我们又不能仅仅只把任务追加到并行队列中之后就不管了,因为这样虽然效率和体验方面改善了,但是不可控,就是说我们不知道什么时候,任务都完成了,所以这根本就达不到我们的需求,在这个时候或许你可以考虑一下dispatch_group, dispatch_group使用方法也比较简单,如下:
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        dispatch_group_t group = dispatch_group_create();
        
        int count = 1000;
        
        dispatch_group_async(group, queue, ^{
            
            for (int i = 0; i < count; i++) {
                
                if (i == count - 1) {
                    NSLog(@"task1 is completion");
                }
            }
        });
        
        dispatch_group_async(group, queue, ^{
            
            for (int i = 0; i < count; i++) {
                
                if (i == count - 1) {
                    NSLog(@"task2 is completion");
                }
            }
        });
        
        dispatch_group_async(group, queue, ^{
            
            for (int i = 0; i < count; i++) {
                
                if (i == count - 1) {
                    NSLog(@"task3 is completion");
                }
            }
        });
       
        dispatch_group_notify(group, queue, ^{
            
            NSLog(@"all task is completion");
        });
    
    

    打印结果:

    group.png

    可以看到最后打印的一定是all task is completion,其他的三个任务是无序的.这完全可以解决上面我们所提的问题

    • 3 . dispatch once
      这个GCD方法是作用是让加入的任务只执行一次.这个方法一个最常见应用的场景就是我们设计模式中的单例模式了.
     static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
            NSLog(@"这里只会打印一次");
      });
    
    • 4 . dispatch barrier
      在日常工作当中,不可避免的会使用到数据库,当多线程与数据库碰到一起,就产生了线程安全问题,当然仅仅是读取操作是不会有影响的,但是一边写入,一边读取呢?串行队列是可以达到目的,但是老调重弹的一个问题是效率太低,那么有没有一种方法能达到读取随意,但是可以控制写入时只写入,不支持读取呢?答案就是dispatch barrier.我们看一下实例:
        NSLog(@"\n");
        
        dispatch_queue_t queue = dispatch_queue_create("com.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(queue, ^{
            
            NSLog(@"task1 for reading");
        });
        
        dispatch_async(queue, ^{
            
             NSLog(@"task2 for reading");
        });
        
        dispatch_async(queue, ^{
             NSLog(@"task3 for reading");
        });
        
        dispatch_async(queue, ^{
             NSLog(@"task4 for reading");
        });
        
        dispatch_barrier_async(queue, ^{
            
             NSLog(@"task8 for writing");
        });
        
        dispatch_async(queue, ^{
             NSLog(@"task5 for reading");
        });
        
        dispatch_async(queue, ^{
             NSLog(@"task6 for reading");
        });
        
        dispatch_async(queue, ^{
             NSLog(@"task7 for reading");
        });
    

    打印结果:

    barrier.png

    dispatch barrier前面的任务和后面的任务是没有顺序的,但是dispatch barrier是有顺序的,就是一定在1,2,3,4之后,在5,6,7之前执行.

    注: dispatch barrier只对自己创建的并行队列有效,对从系统获取的全局队列没有效果,要注意这个坑.

    GCD常见用法就先列举这么多了,相信经过上文这么长的描述,小伙伴对GCD应该有了更深刻的理解了,其实GCD也没有想象中那么神秘,只要慢慢探索,还是可以理解的,套用社会主义老大的一句话就是:2017不要怂,撸起袖子加油干.当然如果在阅读过程中发现有错误的地方,欢迎指正.

    文中所有的案例都在这里,请戳demo地址

    相关文章

      网友评论

      本文标题:GCD大法全解析

      本文链接:https://www.haomeiwen.com/subject/pbykgttx.html