美文网首页
GCD常用API探究总结及死锁发生场景探究

GCD常用API探究总结及死锁发生场景探究

作者: 健指云峰 | 来源:发表于2019-08-07 16:40 被阅读0次

    GCD是iOS开发中常用的多线程工具,它是一套低层级的C API,通
    过 GCD,开发者只需要向队列中添加一段代码块(block或C函数指针),而不需要直接和线程打交道。GCD也可以说是面向队列编程,将我们希望添加的同步、异步任务放到不同的队列中去执行。

    本文目录

    1、常见概念解析
    2、同步异步函数使用方法探究
    3、死锁发生的场景
    4、常用API总结

    一、常见概念解析

    GCD的使用必定绕不开多线程,先来说一说多线程中的一些概念,可以帮助童鞋们更好的理解后面的内容,新手也可以借此扫盲

    Dispatch Queue:执行处理的等待队列,开发者将block添加到队列当中,不需要关心线程的创建等过程(任务添加基于FIFO原则)。队列分为两种,一种是Serial Dispatch Queue(串行队列),另一种是Concurrent Dispatch Queue(并发队列)。
    Serial Dispatch Queue:一次只能执行一个任务,后面的任务必须等待当前任务执行完毕才能开始执行
    Concurrent Dispatch Queue:一次可以执行多个任务,后面的任务不需要等待当前的任务执行完毕
    同步:同步任务创建后必须立即执行,并且堵塞当前线程,当该任务返回后线程继续处理后面的任务
    异步:异步任务创建后立即返回,不会堵塞线程,后面的任务不必等待该任务返回,一般会新创建一个线程来处理这个任务

    二、同步异步函数使用方法探究

    只通过我的叙述可能还是有些迷惑,我们通过代码来帮助理解概念
    1、同步任务+串行队列

    - (void)syncSerial{
        dispatch_queue_t queue = dispatch_queue_create(projectId, DISPATCH_QUEUE_SERIAL);
        
        dispatch_sync(queue, ^{
            NSLog(@"任务一---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_sync(queue, ^{
            NSLog(@"任务二---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_sync(queue, ^{
            NSLog(@"任务三---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_sync(queue, ^{
            NSLog(@"任务四---当前线程%@",[NSThread currentThread]);
        });
        
    }
    
    第一种情况的打印结果

    结论: 同步函数+串行队列,不创建新线程,顺序执行

    2、 同步函数+并发队列

    - (void)syncConcurrent{
        dispatch_queue_t queue = dispatch_queue_create(projectId, DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_sync(queue, ^{
            NSLog(@"任务一---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_sync(queue, ^{
            NSLog(@"任务二---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_sync(queue, ^{
            NSLog(@"任务三---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_sync(queue, ^{
            NSLog(@"任务四---当前线程%@",[NSThread currentThread]);
        });
        
    }
    
    第二种情况的打印结果.png

    结论:同步函数+并发队列 不开启新线程,顺序执行

    3、异步函数+串行队列

    - (void)asyncSerial{
        
        dispatch_queue_t queue = dispatch_queue_create(projectId, DISPATCH_QUEUE_SERIAL);
        
        dispatch_async(queue, ^{
            NSLog(@"任务一---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_async(queue, ^{
            NSLog(@"任务二---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_async(queue, ^{
            NSLog(@"任务三---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_async(queue, ^{
            NSLog(@"任务四---当前线程%@",[NSThread currentThread]);
        });
    }
    
    第三种情况的打印结果.png

    结论: 异步函数+串行队列 开启新线程,顺序执行任务
    4、 异步函数+并发队列

    - (void)asyncConcurren{
        
        NSLog(@"开始任务");
        
        dispatch_queue_t queue = dispatch_queue_create(projectId, DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(queue, ^{
            NSLog(@"任务一---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_async(queue, ^{
            NSLog(@"任务二---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_async(queue, ^{
            NSLog(@"任务三---当前线程%@",[NSThread currentThread]);
        });
        
        dispatch_async(queue, ^{
            NSLog(@"任务四---当前线程%@",[NSThread currentThread]);
        });
    }
    
    第四种情况的打印结果.png

    结论: 异步函数+并发队列 创建新线程,并发执行任务

    通过以上实验可以看出,对于串行队列,任务都是顺序执行的,只不过异步任务会新创建一个线程,同步任务在当前线程,但是多个异步任务都在同一个新创建的线程中。
    对于并发队列,同步任务不会创建新的线程,顺序执行,异步任务会创建多个子线程,而且是并行执行的。
    在这里解释一下并发和并行的区别,并发是指在一个时间段内可以处理多个任务,但这些任务如果以微观视角来看还是顺序执行的,只不过CPU的处理速度极快,我们感觉不出来。而并行是真正的同时处理多个任务,可以想象是多条公路上同时有汽车在跑。
    对于并发队列,虽然可以同时处理多个任务,但还是遵循FIFO原则的。如果最多只能创建四个子线程,分别正在执行四个任务,这时有第五个任务加入了队列,它由最先处理完任务的线程来处理,否则就得等待。也就是说从单个线程的处理过程看是串行的,也是FIFO的。同理多个串行队列同时执行任务也可以认为是并行的。

    另外常用的API还有栅栏函数,它的功能用一句话总结:保证它之前的任务先于它执行,它后面的任务后于它执行,文艺点说是起到承上启下的作用。

    - (void)asyncBarrier{
        dispatch_queue_t queue = dispatch_queue_create("com.example", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(queue, ^{
            NSLog(@"读取任务1");
        });
        
        dispatch_async(queue, ^{
            NSLog(@"读取任务2");
        });
        
        dispatch_async(queue, ^{
            NSLog(@"读取任务3");
        });
        
        dispatch_barrier_async(queue, ^{
            NSLog(@"写入");
        });
        
        dispatch_async(queue, ^{
            NSLog(@"读取任务4");
        });
        
        dispatch_async(queue, ^{
            NSLog(@"读取任务5");
        });
        
        dispatch_async(queue, ^{
            NSLog(@"读取任务6");
        });
        
    }
    
    栅栏函数的使用.png

    通过执行结果可以看到,读取任务1、2、3的执行顺序不固定,但一定优于dispatch_barrier_async中的任务执行,读取任务4、5、6的执行顺序不固定,但一定迟于dispatch_barrier_async中的任务执行。就像栅栏一样将这些任务分割开。
    dispatch_barrier_(a)sync使用时需要注意,它只对我们自己创建的队列有作用,对于系统的队列(主队列、全局队列)和dispatch_(a)sync功能相同。

    三、死锁发生的场景

    造成死锁的四个必要条件:
    互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
    占有并等待:—个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
    非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
    循环等待:有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。
    在iOS开发中发生死锁,一般情况下资源是不可抢占的,而且既然造成了死锁那我们分析的重点在于是如何发生相互等待的。

    在viewDidLoad方法中添加如下代码,这是在主队列中添加同步任务造成的死锁


    主线程死锁.png

    原因就在于viewDidLoad方法的代码块中是一个任务(调用时需要等待返回所以是同步任务,只是个人理解),是系统添加到主队列中的,而在该任务又将图中的一个block(可以看做另一个任务)添加到了主队列中,而且是个同步任务,该任务默认添加到队列尾部。根据之前的概念分析,主队列是一个特殊的串行队列,而且只包含主线程,因此一次只能处理一个任务,后面的任务必须等待当前任务执行完毕才可以执行。block中的任务等待viewDidLoad中的任务执行,而viewDidLoad任务以同步方式添加了block中的任务,需要等待同步任务返回才能继续执行,造成了循环等待。
    如果你还是不明白,请看下面的例子

        dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
        NSLog(@"1"); // 任务1
        dispatch_async(queue, ^{
            NSLog(@"2----当前线程%@",[NSThread currentThread]); // 任务2
            //在当前block这个任务里面创建同步任务,是希望等待该任务返回后再执行后面的内容,即便后面没有代码执行,但是刚添加的任务在队列尾部,等待队列中靠前的任务执行。
            dispatch_sync(queue, ^{
                NSLog(@"3----当前线程%@",[NSThread currentThread]); // 任务3
            });
            NSLog(@"4----当前线程%@--%d",[NSThread currentThread],count);  // 任务4
        });
        NSLog(@"5"); // 任务5
    

    上面这段代码乍一看没什么问题,但是一运行就会报错,不信的小伙伴可以试一试

    同步任务添加不当
    同步任务添加不当打印情况.png
    我们来仔细分析一下上面这段代码这些任务的执行顺序,这样就可以找到到底是哪个地方在循环等待
    首先在主线程中创建了一个串行队列,然后执行任务一,将异步任务添加到串行队列中马上返回(因为是异步所以系统会帮我们创建一个子线程来执行block里面的任务),执行任务五,异步任务在新的子线程中执行任务二,因此任务五任务二的执行顺序不确定。后面就没有主队列的什么事了,我们创建的串行队列中是执行的子线程任务,不会牵扯到主线程。
    任务二执行完毕后,将同步任务添加到串行队列,默认添加到队列尾部,但是在这里为什么不会执行任务三而是发生死锁呢。这说明任务三在等待它之前的任务执行,但恰好被等待的任务同步添加了任务三又在等待任务三执行并返回,所以造成了死锁划重点,判断死锁的思路)。所以我们要找到是哪个任务在任务三前面并且同步添加了任务三。
    其实这里需要仔细区分一下任务这个概念,GCD在添加任务时是以block为单位的,也就是说一个block内的代码块就是一个任务(即使代码块为空也是一个任务,大家可以想一想为什么viewDidLoad里面只有同步往主队列添加任务的代码也会造成死锁了),而我们通常打印的这些任务一、任务二这些是block内的子任务,也可以说是对block内任务的一个划分,只是为了方便描述。回到我们的代码,在串行队列中异步添加的block就是一个任务,当在处理这个任务的时候,它又申请添加了同步任务任务三,需要等待任务三的返回,但是任务三在队尾,等待block任务执行,所以造成了循环等待。
    到此为止就给大家分析完了,当然造成死锁还会有其他情况,不过我暂时没有遇到,我认为只要分析透彻几种典型情况并掌握方法,在以后遇到问题时就不会手足无措了。
    后面是一些常用的API,喜欢的童鞋可以拿走。

    四、常用API总结

    /*
     * Main Dispatch Queue 的获取方法
     */
    dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
    
    /*
     * Global Dispatch Queue (高优先级)的获取方法
     */
    dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
    /*
     * Global Dispatch Queue (默认优先级)的获取方法
     */
    dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    /*
     * Global Dispatch Queue (低优先级)的获取方法
     */
    dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    
    /*
     * Global Dispatch Queue (后台优先级)的获取方法
     */
    dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    
    
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(globalQueue, ^{
        // 一个异步的任务,例如网络请求,耗时的文件操作等等
        ...
        dispatch_async(dispatch_get_main_queue(), ^{
            // UI刷新
            ...
        });
    });
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行一次的任务,例如创建单例
        ...
    });
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, queue, ^{
        // 异步任务1
    });
    
    dispatch_group_async(group, queue, ^{
        // 异步任务2
    });
    
    // 等待group中多个异步任务执行完毕,做一些事情
    dispatch_group_notify(group, mainQueue, ^{
        // 任务完成后,在主队列中做一些操作,相当于做完所有事情后总结,总是在最后执行,和这段代码的添加顺序无关
        ...
    });
    

    参考文章
    GCD容易让人迷惑的几个小问题
    iOS GCD全析(一)(这一系列的文件简直就是我的扫盲篇,大赞作者大大,希望可以帮到像我这样的小白)

    相关文章

      网友评论

          本文标题:GCD常用API探究总结及死锁发生场景探究

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