美文网首页
认识Grand Central Dispatch (GCD)

认识Grand Central Dispatch (GCD)

作者: longjianjiang | 来源:发表于2016-07-31 15:06 被阅读286次

    首先先引用阳神Sunny博客中的几道面试题:

    Snip20160728_1.png

    GCD开发中用的十分广泛,所以有必要进行深入的了解。下面就一步一步的深入下去。


    概述

    说到GCD自然就会想到多线程,GCD是一种异步执行任务的技术,它避免了让程序员直接操作线程的种种麻烦。在GCD中开发者只需要定义一系列的任务放到合适的运行队列中执行即可,这样GCD就会根据情况开若干条线程同时负责线程的生命周期。

    队列

    GCD只有两种类型的队列:
    DISPATCH_QUEUE_SERIAL串行队列
    DISPATCH_QUEUE_CONCURRENT并行队列

    • 两种队列都是dispatch_queue_t类型的对象。
      可以通过如下方法创建(第一个参数用来标识队列方便调试时候查看)。
    dispatch_queue_create("com.longjianjiang.queue", DISPATCH_QUEUE_CONCURRENT);
    
    • 两种队列的执行方式都是按照先进先出的原则,只是串行队列一次只执行一个任务,而并行队列在资源允许的情况下会开线程一次执行多个任务。
    两个特殊的队列
    • dispatch_get_global_queue(long identifier, unsigned long flags)
      系统提供的全局并行队列,可以指定优先级,一般默认选择DISPATCH_QUEUE_PRIORITY_DEFAULT.
    • dispatch_get_main_queue()
      系统提供的主队列(串行队列),也就是提交的任务会在主线程执行.一般更新UI相关会用到主队列。
    队列优先级

    默认我们通过dispatch_queue_create方法创建的队列优先级默认是DISPATCH_QUEUE_PRIORITY_DEFAULT。如果想设置队列的优先级有两种方法。

    • 1.dispatch_queue_attr_make_with_qos_class,如下图:
    Snip20160731_1.png
    该方法通过设置dispatch_queue_attr_t来设置队列的优先级。

    第一个参数 dispatch_queue_attr_t attr:与特定的服务质量类相关联的队列的属性值信息。如果你想让被提交的任务被连续的执行,则指定DISPATCH_QUEUE_SERIAL值,或如果你想让被提交的任务被并发的执行,则指定DISPATCH_QUEUE_CONCURRENT值。如果你传NULL,则此方法默认创建一个连续的队列。
    第二个参数 dispatch_qos_class_t os_class:和队列优先级dispatch_queue_priority_t类似,同样有四种,具体和队列优先级的映射见下图。
    第三个参数int relative_priority:对第二个参数四个特定的服务质量优先级所代表的值的一个偏移,这个值必须不大于于0并且不小于QOS_MIN_RELATIVE_PRIORITY,否则返回为NULL.一般默认为0。

    Snip20160731_2.png
    • 2.dispatch_set_target_queue方法设置优先级
    Snip20160731_3.png

    第一个参数dispatch_object_t object: 要修改的队列,这个参数不能为NULL
    第二个参数dispatch_queue_t queue:有优先级的队列,执行完方法,前一个没有优先级的队列优先级和此队列相同。

    改变多个队列任务的执行顺序
    • dispatch_set_target_queue
    Snip20160731_4.png
    如果我们需要把不同队列中得不同任务按照顺序去执行,例如图中的queue1queue2分别存放两个任务,此时要求输出必须为2134,所以调用dispatch_set_target_queue方法让queue1queue2分别指定目标为串行队列consultQueue,此时原本应该并行执行的四个任务只能一个一个依次执行。

    执行方式

    GCD只有两种执行方式
    dispatch_sync 同步执行
    dispatch_async异步执行

    • 同步执行就是多个任务依次按顺序执行,一个接着一个的执行。
    • 异步执行就是在执行某个任务的时候,不等任务结束就可以返回,其他任务依然可以继续,也就是说异步执行通常会开新线程。

    比如下载一张图片显示,要先从网络上下载图片,然后更新UI。同步方法就是等待图片下载完成再更新UI,而异步则是立刻从图片下载的方法返回并向后执行,此时我们依然可以处理界面上的点击事件,否则主线程就被阻塞了。

    队列和执行方式的组合

    所有组合及情况见下图:


    Snip20160731_5.png

    注意第一种不能用的情况是当前线程在主线程,如果是非主线程的话则是可以的

    // 该方法对当前线程进行判断,从而避免的死锁的发生
    void runOnMainQueueWithoutDeadlocking(void (^block)(void))
    {
        if ([NSThread isMainThread])
        {
            block();
        }
        else
        {
            dispatch_sync(dispatch_get_main_queue(), block);
        }
    }
    

    死锁问题

    下图会导致死锁,为什么?


    Snip20160731_6.png

    主线程是串行的,在执行某一个任务的时候线程被阻塞了,而这个任务(dispatch_sync)在执行时,又要求阻塞主线程,从而导致了互相的阻塞,也就是死锁。

    避免死锁

    除了特定要求需要同步执行,那么我们没有理由不充分利用CPU选择异步执行。

    dispatch_queue_set_specific,dispatch_queue_get_specific,dispatch_get_specific配合使用可以防止在串行队列中的同步任务嵌套一个此队列的同步任务从而导致死锁。

    Snip20160803_1.png
    不过上面仅仅是为了举例,实际中并没有用过,一个比较好的例子就是FMDB中就用了此方法防止死锁的。 Snip20160803_4.png Snip20160803_6.png
    注意:如果不在队列中想要通过key获取到context,得使用dispatch_queue_get_specific传入参数队列才能获取。
    dispatch_get_current_queue

    此方法iOS6中被废弃了,为什么呢?
    首先如果队列调用了dispatch_set_target_queue方法

    dispatch_set_target_queue(queue, targetQueue); 
    

    1.此时如果调用dispatch_get_current_queue,是应该返回queue还是targetQueue呢?
    2.如下图,通过dispatch_get_current_queue方法判断当前队列是否为queueA,如果不是就同步执行一个任务。

    if (queueA == dispatch_get_current_queue()){
        block();
    } else {
        dispatch_sync(queueA,block);
    }
    

    例如同步执行的block如下所示

        dispatch_sync(queueB, ^{
           //此时通过`dispatch_get_current_queue`得到的队列是`queueB`
          //但此时`queueA`是被阻塞的,
          //所以继续执行下面任务就会死锁。
            dispatch_sync(queueA, ^{
                // some task
            });
        });
    

    所以dispatch_set_target_queue使用不当会导致死锁,我们可以使用之前的dispatch_queue_get_specific来实现相关功能。

    附苹果文档的解释:
    Recommended for debugging and logging purposes only: The code must not make any assumptions about the queue returned, unless it is one of the global queues or a queue the code has itself created. The code must not assume that synchronous execution onto a queue is safe from deadlock if that queue is not the one returned by dispatch_get_current_queue().

    三种特殊的执行

    • dispatch_once
      一次执行,大多用来创建单例或者全局的数据。
     + (UIColor *)color {
        static UIColor *color;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            color = [UIColor orangeColor];
        });
        return color;
    }
    
    • dispatch_after
      延迟执行,不过blcok中任务不可以取消,所以建议如果可以的话使用-viewWillAppear-viewDidAppear会更好。

      Snip20160803_8.png
    • dispatch_apply
      类似for循环的一个方法,按指定的次数将指定的block追加到指定的队列中,并等到全部的处理执行结束,默认同步执行,所以传入的队列不能为主队列,否则会死锁。但当传入的时全局队列的时候,执行是异步的 。 同时只有当执行完对应的次数后才会执行下面的代码,所以最后才输出 done。

      Snip20160803_10.png

    dispatch groups

    开发中我们的应用通常会向服务器发送一连串的请求,比如说应用启动的时候会向服务端请求一些配置信息,这些配置信息可能需要多个请求组合而成,而且这些请求彼此之间并没有关联,那么这个时候问题来了,我们如何知道这些任务什么时候执行完成了呢?

    此时你就需要创建一个dispatch_group_t

    dispatch_group_t group = dispatch_group_create();
    

    下面我们可以将之前的任务添加到group中:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_async(group, queue, ^{
            //需要执行的任务
        });
    

    但是当有些任务异步执行,会马上返回,这个时候group就会认为放到group中的任务已经结束,显然不合理。

    这个时候我们可以通过dispatch_group_enter表示要开始某个任务了,结束任务之后需要调用dispatch_group_leave来退出group

        dispatch_group_enter(group);
        [service startWithCompletion:^(response *results, NSError* error){
            // 需要执行的任务
            dispatch_group_leave(serviceGroup);
        }];
    

    最后告诉group任务执行完成

    • 第一种方式:
    dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
    

    第二个参数timeout表示需要等待的时间,系统定义了两个常用的值DISPATCH_TIME_NOWDISPATCH_TIME_FOREVER
    如果使用了第一个值,表示会立即查看是否完成任务,第二个表示会等待任务全部结束。此时会阻塞当前的线程,直到dispatch group中的所有任务完成才会返回.
    返回值如果是0表示group中的任务执行结束,否则就不为0.

    • 第二种方式
       dispatch_group_notify(group, queue, ^{
            //不会阻塞当前线程
        });
    

    两种方式按需求选择即可。

    Using Barriers

    在进行文件读和写或者数据库操作的时候,我们必须保证写数据的时候和修改数据库的时候有且仅有一个线程在操作,此时GCD提供了一个好的方法避免写冲突。
    dispatch_barrier_async用于等待前面的任务执行完毕后自己才执行,而它后面的任务需等待它完成之后才执行。

    Snip20160805_3.png
    dispatch_barrier_sync也可以实现上述功能 Snip20160805_1.png

    不过我们发现输出2222222222222的位置两者不一样,这是因为dispatch_barrier_sync会阻塞当前线程,而dispatch_barrier_async则不会。

    Dispatch Semaphore

    信号量也是用来处理当多个线程对某个资源更新可能产生数据的误操作。

    信号量是一个整形值并且具有一个初始计数值,并且支持两个操作:信号通知和等待。当一个信号量被信号通知,其计数会被增加。当一个线程在一个信号量上等待时,计数会减少,当信号量为0,线程会被阻塞。

    在GCD中有三个函数是semaphore的操作,分别是:
    dispatch_semaphore_create   创建一个semaphore
    dispatch_semaphore_signal   发送一个信号
    dispatch_semaphore_wait    等待信号

        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
        NSMutableArray *array = [NSMutableArrayarray array];
        for (int index = 0; index < 100000; index++) {
            dispatch_async(queue, ^(){
                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//
                NSLog(@"addd :%d", index);
                [array addObject:[NSNumber numberWithInt:index]];
                dispatch_semaphore_signal(semaphore);
            });
        }
    
    注意:
    • dispatch_semaphore_wait的第二个参数和之前的dispatch_group_wait是一样的。返回值如果是0,说明此时信号量大于等于1,可以执行任务,非0的话则说明已处于阻塞状态。
    • 当执行完操作之后应该调用dispatch_semaphore_signal方法,以便其他任务有机会去执行。

    Dispatch 其他

    • dispatch_suspenddispatch_resume
      dispatch_suspend挂起指定的Dispatch Queue。
      dispatch_resume恢复指定的Dispatch Queue。
      两者对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。而恢复则使这些处理能够继续执行。
    • dispatch_main
      该方法可以阻塞主线程,同时必须只能在主线程中调用,否则会导致程序崩溃。

    最后,面试题的答案都有了!

    尾巴

    欢迎关注@longjianjiang,下次再见。

    相关文章

      网友评论

          本文标题:认识Grand Central Dispatch (GCD)

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