iOSiOS程序猿iOS面试

iOS GCD全析(四)

2018-11-15  本文已影响3人  ChinaChong

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



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


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()的原理,与自旋锁不同,却类似于互斥锁,具有线程的排他性。

上一篇下一篇

猜你喜欢

热点阅读