iOS接下来要研究的知识点iOS随笔

我理解的GCD

2018-03-21  本文已影响62人  CoderXLL

一、前期准备

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

同步派发sync

串行队列
放到串行队列里的任务,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基础的那些概念。希望看到这篇文章的小伙伴,认真思考,动手去做。一起探讨问题。

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

上一篇下一篇

猜你喜欢

热点阅读