iOS多线程方案之GCD详解

iOS开发中一共有四种多线程方案:Pthreads、NSThread、GCD、NSOperation。

  1. Pthreads:是线程的 POSIX 标准。该标准定义了创建和操纵线程的一整套 API,是基于 C 语言的框架。
  2. NSThread:是苹果封装后的,完全面向对象的类库。但是线程的生命周期还需要我们手动管理,不常用。
  3. GCD:是苹果为多核并行运算提供的线程方案,它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),基于 C 语言,使用 block,使用方便、灵活。
  4. NSOperation:是苹果对 GCD 的封装,完全面向对象。 NSOperationQueue 可以设置并发队列的依赖关系、执行顺序、最大并发数,更加灵活,解决复杂的线程设计。

GCD和NSOperation对比:

1. GCD更接近底层,处理速度更快;NSOperation是GCD的高级抽象。
2. GCD仅支持FIFO队列,而NSOperationQueue中的队列可以设置优先级、依赖关系、最大并发数。
3. NSOperationQueue支持KVO,我们可以观察任务的执行状态。

GCD详解

任务和队列

任务就是 GCD 的 block 中需要执行的那段代码,有两种执行方式:同步执行(sync)和异步执行(async),两者的主要区别是:是否等待队列的当前任务执行结束,是否具备开启新线程的能力。

  • 同步执行(sync):只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行(async):可以在新的线程中执行任务,具备开启新线程的能力。

队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,有两种队列:串行队列(SDQ)和并发队列(CDQ)。两者的主要区别是:执行顺序不同,以及开启的线程数不同。

  • 串行队列(Serial Dispatch Queue):只开启一个线程,一个任务执行完毕后,再执行下一个任务。
  • 并发队列(Concurrent Dispatch Queue):可以开启多个线程,同时执行多个任务。

并发原理:放到并发队列的任务,GCD 也会按照 FIFO 的方式取出来,它取出来一个就会放到一个线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。

任务和队列的创建

// 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("SerialQueue", DISPATCH_QUEUE_SERIAL);
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 获取主队列(所有放在主队列中的任务,都会放到主线程中执行)
dispatch_queue_t queue = dispatch_get_main_queue();
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 创建同步执行任务
dispatch_sync(queue, ^{
    // 这里放同步执行任务代码
});
// 创建异步执行任务
dispatch_async(queue, ^{
    // 这里放异步执行任务代码
);

任务和队列有四种组合方式,但是只有异步+并发才能实现并发执行任务
iOS多线程方案之GCD详解_第1张图片

队列组:dispatch_group

有时我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完成后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。

// 添加队列组
- (void)addGroupNotify {
    // 创建队列组   
    dispatch_group_t group =  dispatch_group_create();
    // 获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_group_async(group, queue, ^{
        // 添加任务1
        [NSThread sleepForTimeInterval:2]; 
    });
    
    dispatch_group_async(group, queue, ^{
        // 添加任务2
        [NSThread sleepForTimeInterval:2]; 
    });
  
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等异步任务1、任务2都执行完成后,回到主线程执行下面任务
        // 更新UI等
    });
    (不常用等待方法)
    // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
}

延时执行方法:dispatch_after

严格来说:dispatch_after 函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。这个时间并不是绝对准确的,但是可以大致延迟执行任务。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2.0秒后异步追加任务代码到主队列,并开始执行
    });

线程安全方法:dispatch_once

我们在创建单例时经常使用 dispatch_once 函数,它能保证某段代码在程序运行过程中只被执行1次,并且在多线程的环境下,dispatch_once也可以保证线程安全。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
 });

栅栏方法:dispatch_barrier_(a)sync

有时我们需要异步执行两组操作,而且在第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组分割开来,这就需要用到dispatch_barrier_(a)sync 方法在两个操作组间形成栅栏。原理如下图:
iOS多线程方案之GCD详解_第2张图片

dispatch_barrier_sync 代码后边的任务直到 dispatch_barrier_sync 执行完才被追加到队列中;
dispatch_barrier_async 不用等代码执行完,后边的任务也会被追加到队列中,只是Block不会被执行。

GCD信号量:dispatch_semaphore

Dispatch Semaphore,是持有计数的信号量。计数为0时等待,不可通过,计数大于或等于1时,不等待,可通过,且计数减1。
在实际开发中主要用于:保持线程同步,将异步执行任务转换为同步执行任务;保证线程安全,为线程加锁。
Dispatch Semaphore 提供了三个函数:

  • dispatch_semaphore_create:创建一个 Semaphore 并初始化信号总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加1
  • dispatch_semaphore_wait:当信号量为0时就会一直等待(阻塞所在线程),否则正常执行,信号量减1。

线程同步:有个场景:异步执行耗时任务,并使用异步执行的结果进行一些其他的操作。也就是说,将异步执行任务转换为同步执行任务。比如说:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }
        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码,如果每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,是线程安全的;如果有多个线程同时执行写操作(更改变量),那就需要考虑线程同步,否则的话就可能影响线程安全。下面是一个考虑线程安全的售票例子:

/**
 * 线程安全:使用 semaphore 加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)initTicketStatusSave {
    NSLog(@"semaphore---begin");
    
    semaphoreLock = dispatch_semaphore_create(1);
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 当信号量为0时就会一直等待(阻塞所在线程),否则正常执行,信号量减1。
        // 相当于加锁
        dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
        
        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            // 模拟延时
            [NSThread sleepForTimeInterval:0.2];
        } else {
            // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完"); 
            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }
        // 当信号量加1
        // 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }
}

GCD定时器

NSTimer 定时器是在 RunLoop 中实现的,由于RunLoop在处理各种任务,所以会造成计时器不够准确,有时候会相对慢一些。使用 GCD 的计时器方法会让计时器变得更准确,而且 GCD 不受 RunLoop 的 Mode 影响。

// 倒计时
- (IBAction)countdown:(id)sender {
    // 倒计时时间
    __block NSInteger countTime = 59;
    // 获取全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 设置定时器(倒计时间、开始时间、间隔)
    dispatch_source_set_timer(countTime,DISPATCH_TIME_NOW,1.0*NSEC_PER_SEC, 0); 
    // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        
        if(time <= 0) { 
            // 倒计时结束,关闭定时器
            dispatch_source_cancel(timer);
            // 回到主线程更新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                // 设置按钮的样式
                [self.sendBtn setTitle:@"重新发送" forState:UIControlStateNormal];
                self.sendBtn.userInteractionEnabled = YES;
            });
        } else {
        
            // 回到主线程更新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                // 设置按钮的样式      
                [self.sendBtn setTitle:[NSString stringWithFormat:@"重新发送(%ld)",countTime]; forState:UIControlStateNormal];
                // 在倒计时状态下 用户交互关闭,防止再次点击 button 再次倒计时
                self.sendBtn.userInteractionEnabled = NO;
            });
            countTime--;
        }
    });
    // 启动定时器
    dispatch_resume(timer);
}

参考:
iOS详细介绍GCD
关于iOS多线程

你可能感兴趣的:(iOS开发)