认识Grand Central Dispatch (GCD)

首先先引用阳神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,下次再见。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • Managing Units of Work(管理工作单位) 调度块允许您直接配置队列中各个工作单元的属性。它们还...
    edison0428阅读 7,945评论 0 1
  • 最近颇花了一番功夫把多线程GCD人的一些用法总结出来,一来帮自己巩固一下知识、二来希望能帮到对这一块还迷茫...
    人活一世阅读 284评论 1 1
  • 背景 担心了两周的我终于轮到去医院做胃镜检查了!去的时候我都想好了最坏的可能(胃癌),之前在网上查的症状都很相似。...
    Dely阅读 9,229评论 21 42
  • 第二十一天 人说21天能养成一个习惯,今天正好21天。 可是失眠了,睡眠不足的情况下,明天怎么早起? 此刻把文字补...
    云小5阅读 398评论 0 2
  • 1.【太7:6】不要把圣物给狗,也不要把你们的珍珠丢在猪前,恐怕它践踏了珍珠,转过来咬你们。 ​​​ 2.不要论断...
    胡涂格格阅读 1,492评论 0 0