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

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

系列文章传送门:git

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

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

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

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

信号量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的状况。并发

因此答案为app

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学习

官方文档

相关文章
相关标签/搜索