我理解的GCD

作者: CoderXLL | 来源:发表于2018-03-21 00:09 被阅读62次

    一、前期准备

    GCD中有这么几个概念:同步派发sync异步派发async串行队列并行队列
    大家应该都听过这几个名词。但是对于我来说,有很长一段时间对这些概念是稀里糊涂的。所以我先不对这几个名词作书面上的解释,咱们先把实验搞起来。

    二、组合实验

    1. 异步派发+串行队列
    - (void)test1
    {
        // 自己创建的串行队列
        dispatch_queue_t squeue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
        NSLog(@"开始--%@", [NSThread currentThread]);
        dispatch_async(squeue, ^{
            
            NSLog(@"任务1--%@", [NSThread currentThread]);
        });
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"任务2--%@", [NSThread currentThread]);
        });
        NSLog(@"结束--%@", [NSThread currentThread]);
    }
    

    输出:

    2018-03-20 21:02:46.302640+0800 XLLGCDTest[1598:34803] 开始--<NSThread: 0x600000073600>{number = 1, name = main}
    2018-03-20 21:02:46.303010+0800 XLLGCDTest[1598:34803] 结束--<NSThread: 0x600000073600>{number = 1, name = main}
    2018-03-20 21:02:46.303022+0800 XLLGCDTest[1598:34866] 任务1--<NSThread: 0x600000269500>{number = 3, name = (null)}
    2018-03-20 21:02:46.303521+0800 XLLGCDTest[1598:34803] 任务2--<NSThread: 0x600000073600>{number = 1, name = main}
    

    异步派发+串行队列结论:

    • 异步派发async并不会阻塞队列。(即async函数会直接return)
    • 任务1的线程为3,可得知异步派发async+自己创建的串行队列会开启一个新线程
    • 任务2的线程为1,可得知异步派发async+主队列不会开启新线程
    • 任务1先于任务2执行,串行队列有执行的前后顺序,遵循先进先出
    1. 异步派发+并行队列
    - (void)test2
    {
        // 自己创建的并行队列
        dispatch_queue_t cqueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"开始--%@", [NSThread currentThread]);
        dispatch_async(cqueue, ^{
            
            NSLog(@"任务1--%@", [NSThread currentThread]);
        });
        dispatch_async(cqueue, ^{
            
            NSLog(@"任务2--%@", [NSThread currentThread]);
        });
        dispatch_async(cqueue, ^{
            
            NSLog(@"任务3--%@", [NSThread currentThread]);
        });
        NSLog(@"结束--%@", [NSThread currentThread]);
    }
    

    输出:

    2018-03-20 21:24:47.319299+0800 XLLGCDTest[1974:48326] 开始--<NSThread: 0x60400006fc00>{number = 1, name = main}
    2018-03-20 21:24:47.319593+0800 XLLGCDTest[1974:48326] 结束--<NSThread: 0x60400006fc00>{number = 1, name = main}
    2018-03-20 21:24:47.319607+0800 XLLGCDTest[1974:48518] 任务1--<NSThread: 0x60000026d580>{number = 3, name = (null)}
    2018-03-20 21:24:47.319637+0800 XLLGCDTest[1974:48984] 任务3--<NSThread: 0x604000465440>{number = 5, name = (null)}
    2018-03-20 21:24:47.319653+0800 XLLGCDTest[1974:48977] 任务2--<NSThread: 0x600000268b80>{number = 4, name = (null)}
    

    异步派发+并行队列结论

    • 两者结合后,3个block(任务)开辟了3个不同的线程
    • test2这个函数的外部任务先执行完之后,再回头执行了async函数里的block(内部任务)
    • 3个内部任务是同时执行的,没有先后顺序
    1. 同步派发+串行队列
    - (void)test3
    {
        // 自己创建的串行队列
        dispatch_queue_t squeue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
        NSLog(@"开始--%@", [NSThread currentThread]);
        dispatch_sync(squeue, ^{
            NSLog(@"任务1--%@", [NSThread currentThread]);
        });
        dispatch_sync(squeue, ^{
            NSLog(@"任务2--%@", [NSThread currentThread]);
        });
        NSLog(@"结束--%@", [NSThread currentThread]);
    }
    

    输出:

    2018-03-20 21:33:22.028001+0800 XLLGCDTest[2112:53526] 开始--<NSThread: 0x60400007b980>{number = 1, name = main}
    2018-03-20 21:33:22.028492+0800 XLLGCDTest[2112:53526] 任务1--<NSThread: 0x60400007b980>{number = 1, name = main}
    2018-03-20 21:33:22.028858+0800 XLLGCDTest[2112:53526] 任务2--<NSThread: 0x60400007b980>{number = 1, name = main}
    2018-03-20 21:33:22.028952+0800 XLLGCDTest[2112:53526] 结束--<NSThread: 0x60400007b980>{number = 1, name = main}
    

    同步派发+串行队列结论:

    • 并未开启新线程,且发生了线程阻塞(一般不会这么搞,不排除特殊情况。比如启动App的时候,阻塞线程加载数据库数据至指针变量中,达到view快速获取数据的目的)
    1. 同步派发+并行队列
    - (void)test4
    {
        // 自己创建的并行队列
        dispatch_queue_t cqueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"开始--%@", [NSThread currentThread]);
        dispatch_sync(cqueue, ^{
            
            NSLog(@"任务1--%@", [NSThread currentThread]);
        });
        dispatch_sync(cqueue, ^{
            
            NSLog(@"任务2--%@", [NSThread currentThread]);
        });
        NSLog(@"结束--%@", [NSThread currentThread]);
    }
    

    输出:

    2018-03-20 21:39:28.998304+0800 XLLGCDTest[2238:58139] 开始--<NSThread: 0x6000000644c0>{number = 1, name = main}
    2018-03-20 21:39:28.998569+0800 XLLGCDTest[2238:58139] 任务1--<NSThread: 0x6000000644c0>{number = 1, name = main}
    2018-03-20 21:39:28.998859+0800 XLLGCDTest[2238:58139] 任务2--<NSThread: 0x6000000644c0>{number = 1, name = main}
    2018-03-20 21:39:28.999075+0800 XLLGCDTest[2238:58139] 结束--<NSThread: 0x6000000644c0>{number = 1, name = main}
    

    同步派发+并行队列结论:
    首先同步派发意味着:不能开启新线程,任务创建之后必须执行完才return
    并行队列意味着:任务之间不需要排队,具有同时被执行的潜质
    两者结合:即便是并行队列,但是同步派发限制了线程的唯一性,所以并发同时被执行的潜质仍旧发挥不出来。

    三、实现总结

    根据以上四个简单的小测试,我们可以总结一下一开始提出来的那几个名字的概念了。
    异步派发async

    • 具备开启新线程的能力,但是不一定开启新线程。(小伙伴们还记得什么情况下不开启新线程吗,就是test1得出的一条结论,即任务队列为主队列的时候,因为主队列是系统的一个队列,是最为特殊的一个队列)。
    • 交给它的block(任务),GCD底层根据CPU情况分发给任何一个可能的线程去执行,开发者无法控制。
    • async函数会立即return,不会等待任务执行完再return。

    同步派发sync

    • 不具备开启新线程的能力。
    • 交给它的block(任务),只在当前线程执行
    • sync函数必须要等待任务被执行完再return。

    串行队列
    放到串行队列里的任务,GCD会根据FIFO(先进先出)原则,取出一个,执行一个,有序进行。

    并行队列
    放到并行队列里的任务,GCD 也会根据FIFO原则取出来。但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不一定会让所有任务同时执行。

    四、死锁

    以前听说过死锁,包括大学课本中也讲过死锁的概念。但是这里面有很大的一个误区。死锁的原本并不是线程阻塞,而是队列阻塞
    我们上面test3同步派发+串行队列其实已经造成了线程阻塞,但是并没有造成死锁。
    下面再次举个例子进行分析:

    - (void)test5
    {
        NSLog(@"开始--%@", [NSThread currentThread]);
        dispatch_sync(dispatch_get_main_queue(), ^{
           
            NSLog(@"任务1--%@", [NSThread currentThread]);
        });
        NSLog(@"结束--%@", [NSThread currentThread]);
    }
    

    输出结果:

    2018-03-20 23:40:31.170951+0800 XLLGCDTest[4052:124112] 开始--<NSThread: 0x600000076700>{number = 1, name = main}
    

    可以看到只执行了开始,就再也没有响应了。这个就是典型的死锁现象。下面我们就来分析一下为什么会造成死锁。

    1. 首先test5这个函数肯定是主队列下的一个任务,我们把它看成是一个外部任务。而sync对应的block是这个外部任务里的一个内部任务,并且这个内部任务也在主队列中。
    2. 我们已经知道,根据串行队列FIFO原则,内部任务必须要在外部任务执行完之后才能执行。即任务1必须要在test5这个任务执行之后才能执行。
    3. 但是得知道的一点是内部方法不return,外部方法不能执行下一步。
    4. 内部方法要return,根据sync的特性,必须要执行完任务。
    5. 根据2,这个内部任务与外部任务在同一个串行队列,所以要等待外部任务执行之后才能执行。

    这样就造成了一个矛盾,即外部任务因为内部任务不return而没法执行下一步,内部任务因为外部任务没执行完又不能被执行。所以就造成了死锁。

    解决方法:

    1. 将同步派发改为异步派发。因为异步派发不需要任务被执行完就可以return,这样外部任务就可以顺利执行下一行命令。因为外部任务可以顺利被执行完成,接下来就可以执行内部任务了。
    2. 不使用主队列,使用自己创建的串行队列。因为外部任务与内部任务的队列不一致,所以内部任务不受限于FIFO规则,可以顺利被执行,然后return。但是这样会造成线程阻塞。

    再来看一个例子:

    - (void)test6
    {
        // 创建一个串行队列
        dispatch_queue_t  squeue = dispatch_queue_create("标识符", DISPATCH_QUEUE_SERIAL);
        NSLog(@"开始--%@", [NSThread currentThread]);
        dispatch_async(squeue, ^{
            
            NSLog(@"内部开始--%@", [NSThread currentThread]);
            dispatch_sync(squeue, ^{
                NSLog(@"任务1---%@", [NSThread currentThread]);
            });
            dispatch_sync(squeue, ^{
                NSLog(@"任务2---%@", [NSThread currentThread]);
            });
            NSLog(@"内部结束-----%@", [NSThread currentThread]);
        });
        NSLog(@"结束--%@", [NSThread currentThread]);
    }
    

    输出:

    2018-03-20 23:58:06.480048+0800 XLLGCDTest[4300:132976] 开始--<NSThread: 0x60400007bf00>{number = 1, name = main}
    2018-03-20 23:58:06.480479+0800 XLLGCDTest[4300:132976] 结束--<NSThread: 0x60400007bf00>{number = 1, name = main}
    2018-03-20 23:58:06.480529+0800 XLLGCDTest[4300:133014] 内部开始--<NSThread: 0x6000004626c0>{number = 3, name = (null)}
    

    根据输出可以看到在内部开始后,造成了死锁。为什么呢?我们静下心来进行分析。

    1. 执行异步派发操作的时候,test5这个函数相当于外部任务,async下的block相当于内部任务。
    2. 因为异步函数会直接return,所以test5这个任务直接执行。之后再回头执行async下的任务。
    3. 此时async下的任务其实相当于一个外部任务,而sync下的block相当于一个内部任务。
    4. 我们知道sync必须执行完任务后才return。但是他没法执行完任务。为什么呢?因为它的队列与外部任务async下的队列是同一个,且都是串行队列。根据前进先出原则,这个任务必须得排队等候。
    5. async这个任务需要等待sync函数return才能执行下一行命令,而sync函数return必须要执行完任务才行,而sync的任务此时不能被执行,因为这个任务的队列与async的任务队列为同一个串行队列,受限于FIFO规则,就是这么一个环环相扣的原因,导致了死锁。

    五、归纳

    上面根据测试案例,能够很直观的理解GCD基础的那些概念。希望看到这篇文章的小伙伴,认真思考,动手去做。一起探讨问题。

    留个疑问:如果项目需要在不阻塞线程的情况下,并发地对一个变量进行操作。小伙伴们能想到会造成什么后果吗?要用什么方法对这个问题进行解决呢?
    这个问题我们下节再来探索。

    相关文章

      网友评论

      本文标题:我理解的GCD

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