OC进化多线程iOS学习开发

GCD大法全解析

2017-03-04  本文已影响272人  9fda4b908fad

概述

说到多线程,对于做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 . 同步(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...

获取串行队列和并行队列

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

// 全局并行队列
dispatch_get_global_queue()
//主队列(存在于主线程中特殊的串行队列)
dispatch_get_main_queue()
// 串行队列 :  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用法

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并不是在指定时间后执行,而是在指定时间后追加到队列中去

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,其他的三个任务是无序的.这完全可以解决上面我们所提的问题

 static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
        NSLog(@"这里只会打印一次");
  });
    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地址

上一篇 下一篇

猜你喜欢

热点阅读