阅读 960

iOS底层学习 - 多线程之GCD应用篇

经过前两章的学习,我们对多线程和GCD的使用已经有了了解,这章节就来探讨一些GCD在开发中一些常常使用的GCD函数。

系列文章传送门:

iOS底层学习 - 多线程之基础原理篇

iOS底层学习 - 多线程之GCD初探

iOS底层学习 - 多线程之GCD队列原理篇

我们知道GCD除了基本的dispatch_syncdispatch_async用法外,还有一些其他的用法,比如信号量,调度组,延时执行等等。我们来看一下这个使用是怎么应用到我们平常的多线程开发当中的。

信号量dispatch_semaphore

入门小题

首先我们来看一道经典面试题,问题是下面a会输出什么数值

int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++;
    });
}
NSLog(%d,a)
复制代码

下面我们来分析一下,我们发现在主队列中有while任务NSLog任务,所以串行执行,必然是执行完while才会进行打印,如果没有里面dispatch_async任务,那么a必然是5。

但是我们知道dispatch_async是异步执行的并发队列,所以会开辟线程在进行a++操作,且这些线程操作的都是a的同一片内存空间,也就表示当一个线程执行完毕后,此时只在执行的线程上的a值都会变化,所以也就会存在返回的慢,而导致a大于5的情况。

所以答案为

a => 5

这样的写法必然是浪费线程资源的,非常不合理,通常我们进行I/O操作时,都是需要加锁来保证线程安全的,这样数据才不会出错。

下面我们就用信号量的方法来简单处理一下。

信号量的使用

信号量的使用主要有3个方法来搭配。

  1. dispatch_semaphore_create(value):此方法是用来创建信号量,通常加锁的操作时,此时入参0
  2. dispatch_semaphore_wait(): 此方法是等待信号量,会对信号量减1(value - 1),当信号量 < 0时,会阻塞当前线程,等待信号(signal),当信号量 >= 0时,会执行wait后面的代码。
  3. dispatch_semaphore_signal(): 此方法是信号量加1,当信号量 >= 0 会执行wait之后的代码。

注意事项如下

  1. 这3个方法必须搭配使用,缺一不可
  2. dispatch_semaphore_wait()dispatch_semaphore_signal()是成对使用的

了解了信号量的使用时候,我们就可以很好的解决上面的问题了。我们只需要在要加锁的地方,使用dispatch_semaphore_signal()将信号量+1,等待其执行完a++,dispatch_async执行完毕后,在使用dispatch_semaphore_wait()将信号量-1,此时信号量为0,跳出此次阻塞。具体代码如下

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
NSLog(%d,a)
复制代码

此时的打印结果:

5

栅栏函数dispatch_barrier

栅栏概念

当我们有两组异步任务需要有先后顺序的执行的时候,使用栅栏函数可以很好的解决这个问题。虽然这个函数在日常的开发中使用的不是很多,但是这也是一种比较简单直观的解决此类问题的好办法。

栅栏函数有两个API:(dispatch_barrier_asyncdispatch_barrier_sync)

这两个API都会等栅栏前追加到队列中的任务执行完毕后,再将栅栏后的任务追加到队列中,然后等到dispatch_barrier_asyncdispatch_barrier_sync方法前的任务执行完毕后才会去执行后边追加到队列中的任务,简单来说dispatch_barrier_asyncdispatch_barrier_sync将异步任务分成了两个组,执行完第一组后,再执行自己,然后执行队列中剩余的任务。唯一不同的是dispatch_barrier_async不会阻塞线程。

栅栏使用

通过例子来看一下栅栏函数的使用。通过打印结果过我们可以看到,栅栏函数并没有阻塞主线程的调用,但是异步任务2的执行完毕是在异步任务1后面的。说明栅栏并没有阻塞线程,而只是阻塞了队列的执行。

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.wy.barrier1", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"开始");
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务1-%@",[NSThread currentThread]);
    });
    
    /* 2. 栅栏函数 */
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
    });
    NSLog(@"中止");
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务2-%@",[NSThread currentThread]);
    });
    
    NSLog(@"结束");
复制代码

打印结果:

如果将例子中的dispatch_barrier_async换成dispatch_barrier_sync。通过下面的打印结果过可以看出,dispatch_barrier_sync不仅阻塞了线程的执行,也阻塞了队列的执行。整个任务都按照顺序来执行了。但是阻塞主线程的操作还是尽量来避免。

注意事项如下

  1. 栅栏函数最直接的作用: 控制任务执行顺序,同步
  2. dispatch_barrier_async 前面的任务执行完毕才会来到这里
  3. dispatch_barrier_sync 作用相同,但是这个会堵塞线程,影响后面的任务执行
  4. 栅栏函数只能控制同一并发队列

调度组dispatch_group

对于调度组dispatch_group的使用,在日常的开发中时非常多的,主要也是为了解决多线程中多个异步执行之间,顺序执行的问题。

调度组的使用

调度组使用主要有一下几个函数:

  • dispatch_group_create:用来创建一个调度组
  • dispatch_group_async:先把任务添加到队列中,然后将队列方到调度组中
  • dispatch_group_enterdispatch_group_leave:设置进组和出组,必须成对使用,和dispatch_group_async作用相同
  • dispatch_group_wait: 进组任务执行等待时间
  • dispatch_group_notify:执行调度组结束后接下来的任务

dispatch_group_async使用

具体使用的代码如下。可以发现,无论任务一和任务儿耗时多少,都会在全部执行结束后,调用dispatch_group_notify方法,我们一般在此方法中获取到dispatch_get_main_queue主线程,用来刷新UI等操作。

    //创建调度组
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("com.wy.group", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务一");
    });
    
    dispatch_group_async(group, queue1, ^{
        sleep(2);
        NSLog(@"任务二");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"任务执行完成");
    });
    
复制代码

打印结果:

进出组方式使用

使用进出组的方式和dispatch_group_async效果相同,只不过是有了进出组的代码,逻辑更加清晰明了。而且dispatch_group_enterdispatch_group_leave成对使用的,必须先进组再出组,缺一不可

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"任务一");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"任务二");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"所有任务完成,可以更新UI");
    });
复制代码

打印结果:

dispatch_group_wait使用

对于dispatch_group_wait的使用,在平时的开发中可能使用较少,但是它非常的好用。

dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)需要传入两个参数,一个参数是调度组,另一个参数是指定等待的时间。

那么这个指定等待时间是什么意思呢?

这里的等待表示,一旦调用dispatch_group_wait函数,该函数就处理调用的状态而不返回值,只有当函数的currentThread停止,或到达wait函数指定的等待的时间,或Dispatch Group中的操作全部执行完毕之前,执行该函数的线程停止.

  • 当指定timeoutDISPATCH_TIME_FOREVER时就意味着永久等待
  • 当指定timeoutDISPATCH_TIME_NOW时就意味不用任何等待即可判定属于Dispatch Group的处理是否全部执行结束
  • 如果dispatch_group_wait函数返回值不为0,就意味着虽然经过了指定的时间,但Dispatch Group中的操作并未全部执行完毕
  • 如果dispatch_group_wait函数返回值为0,就意味着Dispatch Group中的操作全部执行完毕

这里的应用场景可以在任务一和任务二两个异步任务,也要有先后的执行顺序时,通过wait来阻塞当前线程,只有当执行完一组任务或者超过超时时间后才可以继续向下进行。

通过一个例子来看一下。通过打印结果可以发现,当执行的之间为DISPATCH_TIME_FOREVER或者未超时时,是先执行了任务一后,才执行的任务二,然后回到主线程的。

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"任务一");
        dispatch_group_leave(group);
    });
    
//    long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW,3 * NSEC_PER_SEC));
    long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_FOREVER, 0));
    if (timeout == 0) {
        NSLog(@"回来了");
    }else{
        NSLog(@"等待中 -- 转菊花");
    }
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"任务二");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"所有任务完成,可以更新UI");
    });
复制代码

打印结果:

如果我们把超时时间设置的短一点,比如1秒,long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW,1 * NSEC_PER_SEC));可以发现打印结果不同,和没有进行wait时的打印结果是相同的。

注意事项如下

  1. dispatch_group_enterdispatch_group_leave成对使用的,必须先进组再出组,缺一不可
  2. dispatch_group_wait可以设置等待时间,用来区分异步任务执行
  3. dispatch_group_notify为组任务全部完成后执行的回调,一般在处理主线程逻辑

延迟函数dispatch_after

对于延迟执行的函数dispatch_after的使用肯定是不会陌生的。

但是需要注意的是:dispatch_after方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after 方法是很有效的。

-(void)afterTask{
    NSLog(@"开始");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"执行---%@",[NSThread currentThread]);
    });
}
复制代码

单次函数dispatch_once

对于单次函数大家也非常熟悉。我们再创建单例的时候经常会用到

dispatch_once方法可以保证一段代码在程序运行过程中只被调用一次,而且在多线程环境下可以保证线程安全。

+ (instancetype)shareInstance{
    static WYManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [WYManager alloc]init];
    });
    return instance;
}
复制代码

事件源dispatch_source

概念

dispatch_source是一种基本的数据类型,可以用来监听一些底层的系统事件,在日常的开发中,我们经常使用它来创建一个GCDTimer。但是它还有很多其他的监听类型,通过查看官方文档我们可得以下监听:

  • Timer Dispatch Source:定时调度源。
  • Signal Dispatch Source:监听UNIX信号调度源,比如监听代表挂起指令的SIGSTOP信号。
  • Descriptor Dispatch Source:监听文件相关操作和Socket相关操作的调度源。
  • Process Dispatch Source:监听进程相关状态的调度源。
  • Mach port Dispatch Source:监听Mach相关事件的调度源。
  • Custom Dispatch Source:监听自定义事件的调度源

dispatch_source使用

使用dispatch_source时,通常是先指定一个希望监听的系统事件类型,再指定一个捕获到事件后进行逻辑处理的闭包或者函数作为回调函数,然后再指定一个该回调函数执行的dispatch_queue即可。当监听到指定的系统事件发生时,Dispatch Source会将已指定的回调函数作为一个任务放入指定的队列中执行,也就是说当监听到系统事件后就会触发一个任务,并自动将其加入队列执行。

这里与通常的手动添加任务的模式不同,一旦dispatch_sourcedispatch_queue关联后,只要监听到系统事件,dispatch_source就会自动将任务(回调函数)添加到关联的队列中,直到我们调用函数取消监听。

为了保证监听到事件后回调函数能够都到执行,已关联的dispatch_queue会被dispatch_source强引用。

有些时候回调函数执行的时间较长,在这段时间内Dispatch Source又监听到多个系统事件,理论上就会形成事件积压,但好在Dispatch Source有很好的机制解决这个问题,当有多个事件积压时会根据事件类型,将它们进行关联和结合,形成一个新的事件。

主要使用的API如下:

  • dispatch_source_create: 创建事件源
  • dispatch_source_set_event_handler: 设置数据源回调
  • dispatch_source_merge_data: 设置事件源数据
  • dispatch_source_get_data: 获取事件源数据
  • dispatch_resume: 继续
  • dispatch_suspend: 挂起
  • dispatch_cancle: 取消

自定义GCDTimer

下面我们通过代码,使用dispatch_source来简单实现一个GCDTimer来加深理解。

首先我们创建一个类WYGCDTimer,用来处理定时器的逻辑,相关代码在此

typedef void(^WYGCDTimerBlock)(void);
@interface WYGCDTimer : NSObject

/// 初始化Timer
/// @param interval 时间间隔
/// @param repeat 重复
/// @param completion 回调
+ (WYGCDTimer *)timerWithInterval:(NSTimeInterval)interval
                           repeat:(BOOL)repeat
                       completion:(WYGCDTimerBlock)completion;

/// 开始
- (void)startTimer;

/// 结束
- (void)invalidateTimer;

/// 暂停
- (void)pauseTimer;

/// 恢复
- (void)resumeTimer;

@end
复制代码
@interface WYGCDTimer ()

@property (nonatomic, assign) NSTimeInterval interval;
@property (nonatomic, assign) BOOL repeat;
@property (nonatomic, copy) WYGCDTimerBlock completion;

@property (nonatomic, strong)dispatch_source_t timer;
@property (nonatomic, assign) BOOL isRunning;

@end

@implementation WYGCDTimer

+ (WYGCDTimer *)timerWithInterval:(NSTimeInterval)interval repeat:(BOOL)repeat completion:(WYGCDTimerBlock)completion {
    WYGCDTimer *timer = [[WYGCDTimer alloc] initWithInterval:interval repeat:repeat completion:completion];
    return timer;
}

- (instancetype)initWithInterval:(NSTimeInterval)interval repeat:(BOOL)repeat completion:(WYGCDTimerBlock)completion {
    if (self = [super init]) {
        self.interval = interval;
        self.repeat = repeat;
        self.completion = completion;
        self.isRunning = NO;
        
        ✅// 初始化timer
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
        ✅// 设置timer
        dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, self.interval * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        
        __weak typeof(self) weakSelf = self;
        ✅// 设置回调
        dispatch_source_set_event_handler(self.timer, ^{
            [weakSelf excute];
        });
        
    }
    return self;
}

- (void)excute {
    if (self.completion) {
        self.completion();
    }
}

/// 开始
- (void)startTimer {
    if (self.timer && !self.isRunning) {
        self.isRunning = YES;
        dispatch_resume(self.timer);
    }
}

/// 结束
- (void)invalidateTimer {
    if (_timer) {
        dispatch_source_cancel(_timer);
        self.isRunning = NO;
        _timer = nil;
    }
}

/// 暂停
- (void)pauseTimer {
    if (self.timer && self.isRunning) {
        dispatch_suspend(self.timer);
    }
}

/// 恢复
- (void)resumeTimer {
    if (self.timer && !self.isRunning) {
        dispatch_resume(self.timer);
    }
}

复制代码

自定义dispatch_source

我们刚刚使用了dispatch_source系统提供的定时器,下面我们使用自定义的dispatch_source来实现一个进度条。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    dispatch_queue_t progressQueue = dispatch_queue_create("com.wy.gcdtimer", DISPATCH_QUEUE_CONCURRENT);
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, progressQueue);
    
    __weak typeof(self) weakself = self;
    dispatch_source_set_event_handler(self.source, ^{
        NSUInteger progress = dispatch_source_get_data(self.source);
        if (progress >= 100) {
            progress = 100;
            dispatch_source_cancel(weakself.source);
            weakself.source = nil;
        }
        NSLog(@"percent: %@", [NSString stringWithFormat:@"%ld",progress]);
    });
    
    dispatch_resume(self.source);
    
}
复制代码

我们使用刚刚创建的timer来循环执行,模拟进度条的进度

_timer = [WYGCDTimer timerWithInterval:1 repeat:YES completion:^{
           static NSUInteger _progress = 0;
            _progress += 10;
            if (_progress > 100) {
                _progress = 100;
                [weakself.timer invalidateTimer];
                weakself.timer = nil;
            }
            if (weakself.source) {
                dispatch_source_merge_data(weakself.source, _progress);
            }
        }];
复制代码

打印结果:

参考资料

Dispatch Source学习

官方文档