iOS GCD全析(四)

本文摘录自《Objective-C高级编程》一书,附加一些自己的理解,作为对GCD的总结。



此篇主要包含以下几个方面:

  • dispatch_suspend / dispatch_resume

  • dispatch_once

  • Dispatch Semaphore

    • dispatch_semaphore_t
    • dispatch_semaphore_create()
    • dispatch_semaphore_wait()
    • dispatch_semaphore_signal()


dispatch_suspend / dispatch_resume

当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。例如演算结果被Block截获时,一些处理会对这个演算结果造成影响。

在这种情况下,只要挂起Dispatch Queue即可。当可以执行时再恢复。

dispatch_suspend 函数挂起指定的Dispatch Queue。

dispatch_suspend(queue);

dispatch_resume 函数恢复指定的Dispatch Queue。

dispatch_resume(queue);

这些函数对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。

注:dispatch_suspend 函数和 dispatch_resume 函数都可以用在Dispatch Source,而挂起和恢复的就是dispatch_source_set_event_handler 函数的回调。



dispatch_once

dispatch_once函数是保证在应用程序执行中只执行一次指定处理的API。下面这种经常出现的用来进行初始化的源代码可通过dispatch_once函数简化。

static int initialized = NO;

if (initialized == NO) {
    /*
     * 初始化
     */
    initialized = YES;
}

如果使用dispatch_once函数,则源代码写为:

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
    /*
     * 初始化
     */
});

源代码看起来没有太大的变化。但是通过dispatch_once函数,该源代码即使在多线程环境下执行,也可保证百分之百安全。

之前的源代码在大多数情况下也是安全的。但是在多核CPU中,在正在更新表示是否初始化的标志变量时读取,就有可能多次执行初始化处理。而用dispatch_once函数初始化就不必担心这样的问题。这就是所说的单例模式,在生成单例对象时使用。



Dispatch Semaphore

当并行执行的处理更新数据时即多个线程同时访问同一数据,会产生数据不一致的情况,有时应用程序还会异常结束。虽然使用Serial Dispatch Queue 和dispatch_barier_async函数可避免这类问题,但有必要进行更细粒度的排他控制。

我们来思考一下这种情况:使用两个线程去访问同一个数据,以下代码countNumber最终结果是多少。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    for (int i = 0; i < 10000; i++) {
        self.countNumber = self.countNumber + 1;
        NSLog(@"%d",self.countNumber);
    }
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    for (int i = 0; i < 10000; i++) {
        self.countNumber = self.countNumber + 1;
        NSLog(@"%d",self.countNumber);
    }
});

最终结果并不是20000,这就是典型的线程安全问题。

因为该源代码使用Global Dispatch Queue 更新countNumber属性,所以执行后数据有很高概率并不是实时有效的,程序很可能异常结束。此时应使用Dispatch Semaphore。

Dispatch Semaphore本来使用的是更细粒度的对象,不过本书还是使用该源代码对Dispatch Semaphore进行说明。

Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。所谓信号,类似于过马路时常用的手旗。可以通过时举起手旗,不可通过时放下手旗。而在Dispatch Semaphore中,使用计数来实现该功能。计数为0时等待,计数为1或大于1时代码可通过。

下面介绍一下使用方法。通过dispatch_semaphore_create函数生成Dispatch Semaphore。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

参数表示计数的初始值。本例将计数值初始化为“1”。

dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);

dispatch_semaphore_wait函数等待Dispatch Semaphore的计数值达到大于或等于1。当计数值大于等于1,或者在待机中计数值大于等于1时,对该计数进行减法并从dispatch_semaphore_wait函数返回。第二个参数与dispatch_group_wait函数等相同,由dispatch_time_t类型值指定等待时间。该例的参数意味着永久等待。另外,dispatch_ semaphore_wait函数的返回值也与dispatch_group_wait函数相同。可像以下源代码这样,通过返回值进行分支处理。

dispatch_semaphore_t sem = dispatch_semaphore_create(1);

long result = dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

if (result == 0) {
    
    /*
     * 由于 Dispatch Semaphore 的计数值达到大于等于1
     * 或者在待机中的指定时间内 Dispatch Semaphore 的计数值达到大于等于1
     * 所以 Dispatch Semaphore 的计数值减去1。
     *
     * 可执行需要进行排他控制的处理
     */
}
else {
    /*
     * 由于 Dispatch Semaphore 的计数值为0
     * 因此在达到指定时间为止待机
     */
}

dispatch_semaphore_wait函数返回0时,可安全地执行需要进行排他控制的处理。该处理结束时通过dispatch _semaphore_signal函数将Dispatch Semaphore的计数值加1。

我们在前面的源代码中实际使用Dispatch Semaphore看看。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

/*
 * 生成 Dispatch Semaphore。
 *
 * Dispatch Semaphore 的计数初始值设定为“1”。
 *
 * 保证可访问 countNumber 属性的线程
 * 同时只能有一个
 */

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_async(queue, ^{
    for (int i = 0; i < 10000; i++) {
        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        /*
         * 执行过 dispatch_semaphore_wait 后计数为 0 ,其它线程需要等待 Dispatch Semaphore ,
         *
         * 一直等待,直到 Dispatch Semaphore 的计数值达到大于等于 1 。
         *
         * 由于可访问 countNumber 属性的线程只有一个
         * 因此可以安全的进行更新
         */
        
        self.countNumber += 1;
        
        dispatch_semaphore_signal(semaphore);
        /*
         * 排他控制处理结束,
         * 所以通过 dispatch_semaphore_signal 函数
         * 将 Dispatch Semaphore 的计数值加 1。
         * 如果有通过 dispatch_semaphore_wait 函数
         * 等待 Dispatch Semaphore 的计数值增加的线程,
         * 就由最先等待的线程执行。
         */
        
        NSLog(@"%d",self.countNumber);
    }
});

dispatch_async(queue, ^{
    for (int i = 0; i < 10000; i++) {
        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        
        self.countNumber = self.countNumber + 1;
        
        dispatch_semaphore_signal(semaphore);
        
        NSLog(@"%d",self.countNumber);
    }
});

这样就保证了线程安全,最后的结果是20000。

在没有Serial Dispatch Queue和 dispatch_barrier_async 函数那么大粒度且一部分处理需要进行排他控制的情况下,Dispatch Semaphore 便可发挥威力。

《关于dispatch_semaphore的使用》中有这样的描述:

  停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。

  信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal 就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了dispatch_semaphore_create(long value),调用一次 dispatch_semaphore_signal ,剩余的车位就增加一个;调用一次 dispatch_semaphore_wait 剩余车位就减少一个;

  当剩余车位为0时,再来车(即调用 dispatch_semaphore_wait )就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。

Parse源码浅析系列(一)---Parse的底层多线程处理思路:GCD高级用法中这样描述:

  在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。 更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。

  从 iOS7 升到 iOS8 后,GCD 出现了一个重大的变化:在 iOS7 时,使用 GCD 的并行队列, dispatch_async 最大开启的线程一直能控制在6、7条,线程数都是个位数,然而 iOS8后,最大线程数一度可以达到40条、50条。然而在文档上并没有对这一做法的目的进行介绍。

  笔者推测 Apple 的目的是想借此让开发者使用 NSOperationQueue :GCD 中 Apple 并没有提供控制并发数量的接口,而 NSOperationQueue 有,如果需要使用 GCD 实现,需要使用 GCD 的一项高级功能:Dispatch Semaphore信号量。

接下来我们使用Dispatch Semaphore来控制GCD的并发数

设置并发数为 3,即最多有三条线程同时执行

- (void)viewDidLoad {
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
    
    dispatch_queue_t queue = dispatch_queue_create("com.example", DISPATCH_QUEUE_CONCURRENT);
    
    unsigned int sleepTime = 2;
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 1000; i++) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"%@",[NSThread currentThread]);
            sleep(sleepTime);
            dispatch_semaphore_signal(semaphore);
        }
    });

    dispatch_async(queue, ^{
        for (int i = 0; i < 1000; i++) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"%@",[NSThread currentThread]);
            sleep(sleepTime);
            dispatch_semaphore_signal(semaphore);
        }
    });

    dispatch_async(queue, ^{
        for (int i = 0; i < 1000; i++) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"%@",[NSThread currentThread]);
            sleep(sleepTime);
            dispatch_semaphore_signal(semaphore);
        }
    });
}

我们在代码中让每条线程每两秒执行一次,放慢速度后就容易看出同时有几条线程在执行




我们把dispatch_semaphore_create()参数换成 2,即dispatch_semaphore_create(2),其它代码原封不动,下面是运行结果



由上述结果可以看出,我们给并发队列异步添加了3个任务,如果没有限制的情况下会创建3条子线程同时执行。当我们把dispatch_semaphore_create()参数设为 3 的时候,3 条线程的确同时执行,当我们换成 2 的时候,就只剩下两条线程在同时执行。这就验证了dispatch_semaphore_create()的参数是可以控制并发数的说法。

所以,我们很多时候看到在网上各类博文中出现的dispatch_semaphore_create(1),这种情况大多被当做线程锁来使用是没有问题的。因为参数为1,所以同时执行的线程只能有1个,达到了线程锁要求的效果。按照dispatch_semaphore_create()的原理,与自旋锁不同,却类似于互斥锁,具有线程的排他性。

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

推荐阅读更多精彩内容