iOS话题:GCD-2020-04-28

基本使用

GCD的基本使用场景就是避免耗时任务导致界面卡顿。基本思路:

  1. 将耗时任务放入全局并行队列中执行。
  2. 执行完成之后,再切回主线程,更新界面。
  3. 全部采用异步执行的方式dispatch_async。这是多线程的核心优势:耗时任务不影响主线程响应用户操作。
// 基本使用
- (IBAction)baseButtonTouched:(id)sender {
    self.baseButton.enabled = NO;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"global_queue执行耗时任务,这里sleep 5s");
        NSLog(@"%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:5];
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"回到main_queue,更新界面");
            NSLog(@"%@", [NSThread currentThread]);
            self.baseButton.enabled = YES;
        });
    });
}
企业微信截图_2670d7a9-f92f-4ad4-8b5b-132ad6670b1e.png

这种场景在平时用的最多,也是GCD最有用的地方。非常简洁地避免了界面卡顿。

死锁

  • 同步执行dispatch_sync会阻塞当前线程,要停下来等分派的任务执行完成才能继续。

  • 主线程队列main queue是一种串行队列DISPATCH_QUEUE_SERIAL,遵循先入先出的规则。所以耗时任务会影响用户的交互操作,就是点了没反应。

  • 如果主线程中同步调用在主线程中执行的子任务,会造成“自己等自己”的现象。这个就是“死锁”。

  • 实际执行的之后,会报错EXC_BAD_INSTRUCTION,现象是发生崩溃。

企业微信截图_4c9a84ec-0f64-46a3-b3ea-a30298a3a774.png

GCD死锁及报错提示(EXC_BAD_INSTRUCTION)

  • 在主线程中应该禁止同步执行dispatch_sync,这是脱裤子放屁,主线程本来就是同步执行的。并且还一不小心会造成“死锁”,引起崩溃。

多读单写

可以多个读者同时读取数据,而在读的时候,不能去写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的。

image.png
  • 栅栏函数dispatch_barrier_async的含义是:(1)在栅栏之前分派的任务先执行(要全部执行完毕);(2)执行栅栏函数中的任务;(3)执行展览函数之后分派的任务。
  1. 由于读写都是耗时任务,不能影响主线程的用户界面,所以不论读写,都应该需要异步执行(就是带_async的分派函数)。

  2. 需要一个并行队列。这里要用自定义的并行队列dispatch_queue_create("cn.com.zxs.GCDDemo", DISPATCH_QUEUE_CONCURRENT),不能用全局的匿名队列dispatch_get_global_queue

  3. 读操作,直接放入这个并行队列执行就行了。异步并行,不影响主界面,也能保证效率。

  4. 写操作,放入栅栏函数中。这里要求只有一个写操作。因为在栅栏里,并且是并行队列的情况下,无法保证执行顺序,会引起混乱。如果读写都采用串行队列,那么会导致效率低下。总之,如果有多个写操作,在栅栏里的多个写操作,需要引入其他的同步机制,保证线程安全。
    这里,为了简单起见,这里只有一个写操作。所谓的“单写”。

// 多读单写,栅栏函数
- (IBAction)barrierButtonTouched:(id)sender {
    // 将读写都放入同一个并行队列;注意,这里用dispatch_get_global_queue不行;
    dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.com.zxs.GCDDemo", DISPATCH_QUEUE_CONCURRENT);
    
    // 写之前的读操作,并发执行
    dispatch_async(concurrentQueue, ^{
        NSLog(@"这是写之前的读操作1。");
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"这是写之前的读操作2。");
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"这是写之前的读操作3。");
    });
    
    // 写操作,在栅栏函数中,单独执行
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"这是写操作,在栅栏函数中。");
        [NSThread sleepForTimeInterval:1];
    });
    
    // 写之后的读操作,并发执行
    dispatch_async(concurrentQueue, ^{
        NSLog(@"这是写之后的读操作1。");
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"这是写之后的读操作2。");
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"这是写之后的读操作3。");
    });
}

这里,把单独的一个写操作放在了栅栏中;写之前放了3个读操作;写之后也放了3个读操作

企业微信截图_23c6440d-1a45-4e73-9578-3b51a9793b03.png

任务组

场景:在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面。又或者,多图下载,所有的图片组件都下载完成之后,到主线程合成一张大图。这个过程类似于Promise.all()

  1. 下载或者网络请求都是耗时任务,会影响主线程,所以需要异步执行;

  2. 各个网络请求是相互独立的,不应该等待,所以需要一个并行队列;

  3. 所有的耗时的并行任务完成之后,需要做更新界面,合并大图之类的操作。这些需要在主线程中完成。所以,这里采用任务组dispatch_group

虽然用dispatch_barrier_async也能完成类似任务,不过用dispatch_group感觉更合适

// 任务组
- (IBAction)groupButtonTouched:(id)sender {
    // 网络任务开始前,修改界面
    self.groupButton.enabled = NO;
    NSLog(@"网络组下载任务开始了... ...");
    
    // 子任务都放入同一个并行队列,保证效率
    dispatch_queue_t concurrentQueue = dispatch_queue_create("cn.com.zxs.GCDDemo", DISPATCH_QUEUE_CONCURRENT);
    
    // 任务组
    dispatch_group_t group = dispatch_group_create();
    
    // 子任务派发;异步形式
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"完成子任务1");
    });
    
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"完成子任务2");
    });
    
    dispatch_group_async(group, concurrentQueue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"完成子任务3");
    });
    
    // 子任务都完成后,更新界面等后续工作
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"完成所有子任务,更新界面");
        self.groupButton.enabled = YES;
    });
}
企业微信截图_40d4e248-d4a8-44f9-82ab-8e7fe78cdfba.png

线程同步

初始信号量为0的dispatch_semaphore可以用来做线程同步。
场景:任务A不需要等待;任务B第1、3部分不需要等待,第2部分需要等待任务A先完成;任务C需要等待任务B的第2部分先完成。

// 线程同步
- (IBAction)synchronizeButtonTouched:(id)sender {
    // 同步任务开始前,修改界面
    self.synchronizeButton.enabled = NO;
    
    // 一开始,AB都没有完成,没有信号
    dispatch_semaphore_t semaphoreA = dispatch_semaphore_create(0);
    dispatch_semaphore_t semaphoreB = dispatch_semaphore_create(0);
    
    // ABC同时开始;异步执行的并行队列,保证效率
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"任务A不需要等待,开始...");
        sleep(2);
        NSLog(@"任务A完成,设置semaphoreA");
        dispatch_semaphore_signal(semaphoreA);
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"任务B第1部分不需要等待,开始...");
        sleep(1);
        NSLog(@"任务B第1部分完成,等待任务A的完成信号semaphoreA");
        dispatch_semaphore_wait(semaphoreA, DISPATCH_TIME_FOREVER);
        NSLog(@"收到任务A完成的信号,任务B第2部分开始...");
        sleep(2);
        dispatch_semaphore_signal(semaphoreB);
        NSLog(@"任务B第2部分完成,设置semaphoreB;第3部分,开始...");
        sleep(6);
        NSLog(@"任务B第3部分完成。耗时较长,估计排最后面了。");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"要等任务B第2部分完成,任务C开始不了!!!!");
        dispatch_semaphore_wait(semaphoreB, DISPATCH_TIME_FOREVER);
        NSLog(@"任务B第2部分终于来了,任务C开始...");
        [NSThread sleepForTimeInterval:0.5];
        NSLog(@"任务C完成!");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"任务C负责更新界面,不管任务B第3部分!!!!");
            self.synchronizeButton.enabled = YES;
        });
    });
}
企业微信截图_e43aab47-5793-408e-b9c2-cebdbab260c4.png

线程安全

初始信号量为1的dispatch_semaphore可以用来当做锁,对只能独占访问的关键资源进行保护。
场景:假设银行里本来有钱1000;现在有4个人同时来取钱。前面一些操作是并发的,比如输入密码之类的。这里用sleep(5)来模拟。
取钱过程,需要独占访问,所以用一个初始信号量为1的dispatch_semaphore进行保护;

  1. 查询余额;
  2. 取钱过程,这里用sleep(2)来模拟;
  3. 取钱,同时查询余额;
// 线程安全
- (IBAction)safeButtonTouched:(id)sender {
    // 值为1的semaphore相当于锁
    dispatch_semaphore_t lockSemaphore = dispatch_semaphore_create(1);
    // 钱只有一份,是大家公用的,需要加锁,保证线程安全
    __block NSInteger money = 1000;
    
    // 10个人同时来取钱
    for (int i = 0; i < 4; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"第%d个人开始操作,初始金额:%ld===", (i + 1), money);
            sleep(5);
            // 访问关键资源,用锁保护
            dispatch_semaphore_wait(lockSemaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"第%d个人开始取钱;当前余额:%ld...", (i + 1), money);
            sleep(2);
            money -= 100;  // 每人每次取10元
            NSLog(@"第%d个人取钱完毕;当前余额:%ld", (i + 1), money);
            dispatch_semaphore_signal(lockSemaphore);
        });
    }
}
企业微信截图_e3f12238-8588-49d7-86ac-a2823dfa10f5.png

如果不保护,那么金额完全就乱了。(就是注释掉有关dispatch_semaphore的几句)

企业微信截图_4b9fd79e-0246-4680-b38d-720b209334b8.png

signal的调用次数一定要大于等于wait的调用次数,否则导致崩溃。
dispatch_semaphore使用崩溃问题

延时执行

延时执行,我们往往会想到sleep()函数。这样做虽然能达到目的,不过这会阻塞当前线程,效率低下。如果在主线程使用sleep(),那么在这段延时期间,界面将失去响应。
dispatch_after就是专门做延时执行的,异步的,不会阻塞当前线程,效果很好。

- (IBAction)delayButtonTouched:(id)sender {
    NSLog(@"延时执行开始....");
    self.delayButton.enabled = NO;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"延时执行结束");
        self.delayButton.enabled = YES;
    });
}

单次执行

在语义上有单个的业务,采用单例模式是非常方便的。比如,常用的个人信息,就可以保留在一个单例之中。
那么如何保证单例的实现呢?
dispatch_once顾名思义,就是只执行一次,这和单例的语义一致,所以非常方便。

新建一个单例SingleDemo,其中获取实例的方法如下:

// 获取单例对象
+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static id instance = nil;
    
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    
    return instance;
}

所有的单例都可以这么写,直接固定为一个宏也可以。

可以测试一下,是否只有一个实例:

// 单次执行
- (IBAction)onceButtonTouched:(id)sender {
    SingleDemo *single1 = [SingleDemo sharedInstance];
    NSLog(@"对象single1地址:%@", single1);
    
    SingleDemo *single2 = [SingleDemo sharedInstance];
    NSLog(@"对象single2地址:%@", single2);
    
    SingleDemo *single3 = [SingleDemo sharedInstance];
    NSLog(@"对象single3地址:%@", single3);
}
企业微信截图_582b790f-3f61-4029-9faf-ae054baac244.png

定时器

大多数时候,用NSTimer就可以了。不过NSTimerRunLoop相关联,定时精度受影响。所以有些时候需要用到GCD定时器。
iOS - GCD 实现定时器、倒计时
iOS GCD定时器使用及封装

  • Xcode对于GCD有代码提示,很方便
image.png
企业微信截图_24987582-d87a-4e70-b8cd-12d0ee00b840.png
  • timer不能是临时变量,需要长久存在,不然block不会执行。比如作为属性:
// 定时器
@property (strong, nonatomic) dispatch_source_t timer;
  • 可以将定时器设置在并行队列中,不会影响主线程的界面
// 定时器
- (IBAction)timeButtonTouched:(id)sender {
    __block NSInteger index = 0;
    // 将定时器设置在并行队列,不影响主线程的界面
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
    dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(self.timer, ^{
        index++;
        NSLog(@"定时器任务第%ld次执行", (long)index);
    });
    dispatch_resume(self.timer);
}
  • 设置之后,会马上执行一次block,不会等下一个周期。
企业微信截图_5181ef09-3105-46a2-86a9-21bb2ddb1e51.png

参考文章

2019 iOS面试题-----多线程相关之GCD、死锁、dispatch_barrier_async、dispatch_group_async、Dispatc

Demo地址

GCDDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。