手撕iOS底层30 -- GCD之函数与队列

1,517 阅读7分钟

有坑位的小伙伴内推我哦

本章开始初探GCD,理解队列与同步异步函数的搭配使用


0x00 - GCD简介

  • 全称Grand Central Dispatch
  • iOS4引入的多线程编程技术
  • 纯c语言编写, 提供非常强大的函数,也就是说用难以置信的非常简洁的记述方法,实现极为复杂繁琐的多线程编程,可以说是一项划时代的技术。

0x01 - GCD特性

  • GCD是苹果公司为多核的并行运算提出的解决方案
  • GCD自动利用更多的cpu内核(比如双核 四核)
  • GCD自动管理线程生命周期 (创建线程, 调度任务,销毁线程)

总结一句话就是将任务添加到队列,并指定任务执行的函数

0x02 - GCD使用

//方式一
		// 创建block任务
    dispatch_block_t block = ^{
        NSLog(@"GCD基本使用");
    };
    // 创建queue
    dispatch_queue_t q = dispatch_queue_create("com.grz.q1", NULL);
    // 将任务加入到队列
    dispatch_async(q, block);

// 方式二
dispatch_async( dispatch_queue_create("com.CJL.Queue", NULL), ^{
   NSLog(@"GCD基本使用");
});
  • 使用dispatch_block_t创建任务
  • 使用dispatch_queue_t创建队列
  • 将任务block加到队列q,指定任务的执行方式dispatch_async

⚠️⚠️⚠️ 注意三连:

typedef void (^dispatch_block_t)(void);

  • 任务使用block封装
  • 任务的block没有参数也没有返回值

总结就是 任务 + 队列 + 函数构成。

0x03 - 函数与队列

函数

GCD执行任务的方式有俩种:

  • 同步执行 dispatch_sync
    • 等待当前语句执行完,才会执行下边的语句
    • 会阻塞当前线程
    • 不具备开启新线程能力
    • 当前线程中执行block任务
  • 异步执行 dispatch_async
    • 不用等待当前语句执行完毕,才会执行下边的语句
    • 会开启线程执行block任务,即具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关)
    • 异步是线程的代名词

队列

开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。

多线程中说的队列(Dispatch Queue)指的是执行任务的等待队列,队列是一种特殊的线性表,遵循FIFO先进先出的原则, 也就是新任务总是被插到队尾, 从队首读取任务,没读取一个任务,从队列中释放一个任务。

2种队列

串行队列(Serial Dispatch Queue)

等待现在执行中处理结束,一次只有一个任务被执行,等待上一个任务执行完毕后再执行下一个,也就是只有一个线程, 同一时刻只调度一个任务执行

  • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);创建串行队列
  • 其中的DISPATCH_QUEUE_SERIAL也可以使用NULL表示,这两种均表示 默认的串行队列

#define DISPATCH_QUEUE_SERIAL NULL

// 串行队列的获取方法 以下相同效果
dispatch_queue_t serialQueue1 = dispatch_queue_create("com.test.Queue", NULL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("com.test.Queue", DISPATCH_QUEUE_SERIAL);

并发队列(Concurrent Dispatch Queue)

一次并发执行多个任务, 开启多个线程, 同时执行任务,同一时刻可以调度多个任务执行)

但并行执行的处理数量取决于当前系统的状态, 即iOS基于Dispatch Queue中的处理数,cpu核数以及CPU负荷等当前系统的状态来决定Concurrent Dispatch Queue中并发

  • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);创建并发队列
  • 注意:并发队列的并发功能只有在异步函数下才有效
// 并发队列的获取方法
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.Queue", DISPATCH_QUEUE_CONCURRENT);


GCD中,针对串行并发,系统提供了主队列(Main Dispatch Queue)全局并发队列(Global Dispatch Queue)

主队列 Main Dispatch Queue

  • 主队列 , Main Dispatch Queue,正如其名称Main一样,实在主线程中执行的Dispatch Queue,因为主线程只有一个,所以Main Dispatch Queue也是Serial Dispatch Queue,追加到主队列的处理在主线程的Runloop执行,由于在主线程中执行, 因此要将一些用户界面更新等一些必须在主线程中执行的处理追加到追队列使用,正好与NSObject中的performSelectorOnMainThread实例方法这一执行方法相同。
  • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度,也就是先执行主线程的任务。
  • 使用dispatch_get_main_queue()获得主队列
//主队列的获取方法
dispatch_queue_t mainQueue = dispatch_get_main_queue();

全局队列Global Dispatch Queue

  • 为了方便程序员的使用,苹果提供了默认的并发队列
  • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列
  • 使用dispatch_get_global_queue获取全局并发队列,最简单的是dispatch_get_global_queue(0, 0)
    • 第一个参数表示队列优先级,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0,在ios9之后,已经被服务质量(quality-of-service)取代
    • 第二个参数使用0
//全局并发队列的获取方法
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

//优先级从高到低(对应的服务质量)依次为
- DISPATCH_QUEUE_PRIORITY_HIGH       -- QOS_CLASS_USER_INITIATED
- DISPATCH_QUEUE_PRIORITY_DEFAULT    -- QOS_CLASS_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW        -- QOS_CLASS_UTILITY
- DISPATCH_QUEUE_PRIORITY_BACKGROUND -- QOS_CLASS_BACKGROUND

0x04 - 函数和队列的搭配使用

01 - 同步函数 + 串行队列

  • 不会开启线程
  • 串行队列中顺序执行
  • 会产生堵塞

02 - 同步函数 + 并发队列

  • 不会开启新线程,当前线程执行任务
  • 任务一个接着一个
  • 串行队列中顺序执行

03 - 异步函数 + 串行队列

-

  • 开启新线程
  • 任务按顺序执行

04 - 异步函数 + 并发队列

  • 开启新线程,在当前线程执行任务
  • 任务异步执行,乱序执行任务,CPU调度有关

05 - 主队列 + 同步函数会发生死锁

具体分析:

dispatch_sync函数是在主线程中执行的,也就是说本身也是主线程的一部分,主线程的特点是,主线程的代码执行完毕后才会执行主队列的任务,而dispatch_sync函数是要等待任务执行完返回,任务不执行,dispatch_sync不返回。

这样,dispatch_sync为了返回会等任务的执行完毕,而任务又等主线程的代码执行完去调度它,主线程又在等dispatch_sync的返回。所以发生死锁

解决方法

把死锁的方法放到子线程去执行

06 - 主队列 + 异步方法

任务顺序执行,不开辟新线程

07 - 全局并发队列 + 同步函数

  • 任务顺序执行
  • 不开辟新线程

08 - 全局并发队列 + 异步函数

  • 开辟新线程
  • 乱序执行任务

09 - 全局并发队列 + 主队列 配合使用

在日常开发中,全局队列+并发并列一般是这样配合使用的

//主队列 + 全局并发队列的日常使用
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //执行耗时操作
        dispatch_async(dispatch_get_main_queue(), ^{
            //回到主线程进行UI操作
        });
    });

总结

函数\队列串行队列并发队列主队列全局并发队列
同步函数顺序执行,不开辟线程顺序执行,不开辟线程死锁顺序执行,不开辟线程
异步函数顺序执行,开辟线程乱序执行,开辟线程顺序执行,不开辟线程乱序执行,开辟线程

0x05 - GCD面试题

01 - 异步函数+并行队列

  dispatch_queue_t queue = dispatch_queue_create("com.test.ggg", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1-%@",[NSThread currentThread]);

    // 耗时
    dispatch_async(queue, ^{
        NSLog(@"2-%@",[NSThread currentThread]);
        dispatch_async(queue, ^{
            NSLog(@"3-%@",[NSThread currentThread]);
        });
        NSLog(@"4-%@",[NSThread currentThread]);
    });
    //sleep(2); //  主队列有大的耗时 或者卡顿就会执行2
    NSLog(@"5-%@",[NSThread currentThread]);
----------打印结果-----------
输出顺序为:1 5 2 4 3

主要是异步函数不会阻塞主线程

02 - 异步函数嵌套同步函数 + 并发队列

- (void)interview04{
    //并发队列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{ // 耗时
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    
    // 同步
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    
    NSLog(@"0");

    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });
}

----------打印结果-----------
输出顺序为:(1 2 3 无序)0(7 8 9 无序),可以确定的是 0 一定在3之后,在789之前

03 - 下面代码中,队列的类型有几种?

//串行队列 - Serial Dispatch Queue
dispatch_queue_t serialQueue = dispatch_queue_create("com.CJL.Queue", NULL);
    
//并发队列 - Concurrent Dispatch Queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);
    
//主队列 - Main Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
//全局并发队列 - Global Dispatch Queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

队列总共有两种: 并发队列串行队列

  • 串行队列:serialQueue、mainQueue
  • 并发队列:concurrentQueue、globalQueue

参考

iOS 多线程GCD主线程上同步执行主队列方法卡死的解释

Objective-C高级编程 iOS与OS X多线程和内存管理

欢迎大佬留言指正😄,码字不易,觉得好给个赞👍 有任何表达或者理解失误请留言交流;共同进步;