17-探究iOS底层原理|多线程技术【GCD源码分析1:主队列、串行队列&&并行队列、全局并发队列】

1,344 阅读13分钟

前言

之前,我们在探索动画及渲染相关原理的时候,我们输出了几篇文章,解答了iOS动画是如何渲染,特效是如何工作的疑惑。我们深感系统设计者在创作这些系统框架的时候,是如此脑洞大开,也 深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。

因此我们决定 进一步探究iOS底层原理的任务。继前面三篇文章对多线程知识进行了简单的回顾之后:

一、对GCD的快速回顾

因为NSOperationNSOperationQueue是对GCD的OC封装,因此我们在这边探索底层原理是直接从GCD入手。

在 探索 GCD的底层实现之前,我们快速过一遍 GCD相关的知识点,如果你想更全面了解GCD也可以参考我的这篇文章:3-多线程方案 GCD

1. GCD的队列

1.1 GCD的队列可以分为2大类型

  • 并发队列(Concurrent Dispatch Queue)
    • 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
    • 并发功能只有在异步dispatch_async函数下才有效
  • 串行队列(Serial Dispatch Queue)
    • 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
// 1.创建队列
/*
  第一个参数:C语言的字符串,标签
  第二个参数:队列的类型
  DISPATCH_QUEUE_CONCURRENT:并发
  DISPATCH_QUEUE_SERIAL:串行
*/

// 并发队列
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_CONCURRENT);
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_SERIAL);


// 2.获得全局并发队列
// 第一个参数:可以设置优先级
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);


// 3.获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();


// 4.异步函数
dispatch_async(queue, ^{
   NSLog(@"download1----%@",[NSThread currentThread]);
});
    
    
// 5.同步函数
 dispatch_sync(queue, ^{
   NSLog(@"download2----%@",[NSThread currentThread]);
});  

1.2 同步、异步、并发、串行的注意点:

  • 同步和异步主要影响:能不能开启新的线程
    • 同步:在当前线程中执行任务,不具备开启新线程的能力
    • 异步:在新的线程中执行任务,具备开启新线程的能力
  • 并发和串行主要影响:任务的执行方式
    • 并发:多个任务并发(同时)执行
    • 串行:一个任务执行完毕后,再执行下一个任务

创建一个同步串行队列

// 不论是哪种队列,都不会开启新线程
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_SERIAL);
// dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t queue = dispatch_get_main_queue();
    
dispatch_sync(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
});
    
dispatch_sync(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
});

// 打印输出:
// <NSThread: 0x6000020198c0>{number = 1, name = main}
// <NSThread: 0x6000020191c0>{number = 1, name = main}

创建一个异步并发队列

// 并发队列
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
});
    
dispatch_async(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
});

// 打印输出:
// <NSThread: 0x6000020198c0>{number = 4, name = (null)}
// <NSThread: 0x6000020191c0>{number = 5, name = (null)}

创建一个异步串行队列

// 串行队列
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_SERIAL);
    
dispatch_async(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
});
    
dispatch_async(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
});

// 打印输出:
// <NSThread: 0x6000020198c0>{number = 5, name = (null)}
// <NSThread: 0x6000020191c0>{number = 5, name = (null)}

在主队列中,不论是同步还是异步都不会开启子线程

dispatch_queue_t queue = dispatch_get_main_queue();

dispatch_async(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
});

// 打印输出:
// <NSThread: 0x6000020198c0>{number = 1, name = main}

但是使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)

综上所述可以用一张图来概述

1.3 dispatch_get_global_queuedispatch_queue_create的区别

我们在代码里分别创建两种队列,然后打印发现,全局队列的地址都是同一个,而dispatch_queue_create的对象都不相同

dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue3 = dispatch_queue_create("queu3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue4 = dispatch_queue_create("queu4", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue5 = dispatch_queue_create("queu5", DISPATCH_QUEUE_CONCURRENT);

NSLog(@"%p %p %p %p %p", queue1, queue2, queue3, queue4, queue5);

// 分别输出:0x10c5d8080 0x10c5d8080 0x6000037c3180 0x6000037c1580 0x6000037c3200

2. GCD的队列组

第一种创建方式

// 1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
// 2.创建队列组
dispatch_group_t group = dispatch_group_create();
   
// 3.把任务添加到队列中 
dispatch_group_async(group, queue, ^{
  	NSLog(@"1----%@",[NSThread currentThread]);
});
    
    
dispatch_group_async(group, queue, ^{
   	NSLog(@"2----%@",[NSThread currentThread]);
});

// 4.拦截通知,当队列组中所有的任务都执行完毕的时候回进入到下面的方法
dispatch_group_notify(group, queue, ^{     
    NSLog(@"-------dispatch_group_notify-------");
});

第二种创建方式

// 1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
// 2.创建队列组
dispatch_group_t group = dispatch_group_create();
    
// 3.在该方法后面的异步任务会被纳入到队列组的监听范围,进入群组
// dispatch_group_enter|dispatch_group_leave 必须要配对使用

dispatch_group_enter(group);
    
dispatch_async(queue, ^{
   NSLog(@"1----%@",[NSThread currentThread]);
        
   //离开群组       	 
   dispatch_group_leave(group);
});

    
dispatch_group_enter(group);
    
dispatch_async(queue, ^{
   NSLog(@"2----%@",[NSThread currentThread]);
        
   //离开群组       	 
   dispatch_group_leave(group);
});
    
    
// 拦截通知
// 内部本身是异步的
dispatch_group_notify(group, queue, ^{
	NSLog(@"-------dispatch_group_notify-------");
});

第三种方式

// 1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
// 2.创建队列组
dispatch_group_t group = dispatch_group_create();
   
// 3.把任务添加到队列中 
dispatch_group_async(group, queue, ^{
  	NSLog(@"1----%@",[NSThread currentThread]);
});
    
    
dispatch_group_async(group, queue, ^{
   	NSLog(@"2----%@",[NSThread currentThread]);
});

// 4.会阻塞线程
// 直到队列组中所有的任务都执行完毕之后才能执行
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

NSLog(@"----end----");

3. 队列函数的应用

讨论一下以下的函数执行顺序是怎样的,检验对GCD的掌握程度

- (void)textDemo2{
    // 同步队列
    dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        // 同步
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

image.png

我们知道无论同步还是异步函数都是一个耗时任务。

串行和并发.png

再来一个,据说这个是新浪的面试题

- (void)wbinterDemo{
    dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", 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");
    });
    // A: 1230789
    // B: 1237890
    // C: 3120798
    // D: 2137890
}

正确答案是A 分析:首先开启的是一个串行队列,12行的代码阻塞的是13行以下的,所以3在0之前,123没有顺序,789也没有顺序,使用排除法得到A

4. 死锁问题

如果把队列修改为串行队列那么此时调用的顺序为:

- (void)textDemo1{
    dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

此时打印崩溃:

image.png

死锁问题.png

此时在这里:

  • 2执行完之后,执行到了一个代码块(dispatch_syncsync的特点是阻塞,必须等到自己执行完之后才可以)
  • 而队列由于是先进先出的原则,所以此时造成了4等待块执行完成
  • 块的执行完成需要3执行,而3又等待4执行,这样就造成了一个死锁的问题。

改进:那么我们把4的任务删除,还会造成死锁嘛?

  • 答案是:还会死锁 观察调用栈发现死锁的函数是:_dispatch_sync_f_slow
  • 实际上发生死锁的dispatch_asyncdispatch_sync这两个代码块

5. GCD创建队列四种方式

    // OS_dispatch_queue_serial 串行
    dispatch_queue_t serial = dispatch_queue_create("hb", DISPATCH_QUEUE_SERIAL);
    // OS_dispatch_queue_concurrent 并发
    dispatch_queue_t conque = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    // DISPATCH_QUEUE_SERIAL max && 1
    // queue 对象 alloc init class
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);

我们知道dispatch_get_main_queue是一个串行队列并且这个队列是在main()调用之前主线程自动创建的,dispatch_get_global_queue是一个并发队列。 打印输出可以得到这些信息: image.png

6. 其他常用方法

// 1.延迟执行的几种方法
// 1.1
[self performSelector:@selector(task) withObject:nil afterDelay:2.0];

// 1.2
// repeats:是否重复调用
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];

// 1.3
// 可以设置队列控制在哪个线程执行延迟
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), queue, ^{
   NSLog(@"GCD----%@",[NSThread currentThread]);
});


// 2.一次性代码
// 整个程序运行过程中只会执行一次
// onceToken用来记录该部分的代码是否被执行过

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
  NSLog(@"---once----");
});


// 3.快速遍历
// 开多个线程进行遍历
/*
   第一个参数:遍历的次数
   第二个参数:队列(并发队列)
   第三个参数:index 索引
*/
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
   NSLog(@"%zd---%@",index,[NSThread currentThread]);
});

// 4.栅栏函数
// 栅栏函数不能使用全局并发队列
// 栅栏函数之后的线程都会延后执行
dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
});    
    
dispatch_barrier_async(queue, ^{
   NSLog(@"+++++++++++++++++++++++++++++");
});
    
dispatch_async(queue, ^{
   NSLog(@"%@", [NSThread currentThread]);
}); 

二、探索GCD的底层实现

苹果官方是对GCD做了开源的,所以我们可以去下载一下GCD源码,阅读学习一下。

1. 源码下载

我们可以通过GCD的源码libdispatch.dylib来分析内部实现

libdispatch.dylib的下载地址:opensource.apple.com/release/mac…

然后找到libdispatch-1173.0.3进行下载

2.源码分析

2.1 主队列分析

查看主队列的 api如下图: dispatch_get_main_queue

  • 主队列是一个特殊的串行队列
  • 主队列在调用main()函数之前自动创建的。
  • 主队列在应用程序上下文中用于与主线程和main runloop 交互。

那么断点在main 函数处去验证一下

验证main_queue

通过断点,确实验证了主队列是在调用main()函数之前自动创建的。

那么我们要看底层源码,该怎么看啊,首先我们得知道 GCD是属于哪个源码的,才能进一步去探索分析。

  • 再次通过断点寻找,如下图

寻找 GCD源码出处 通过 bt 打印堆栈信息,可以定位到libdispatch.dylib动态库,那么就去苹果开源网站去下载源码试试。

libdispatch

  • libdispatch 源码

GCD源码libdispatch

libdispatch源码阅读起来比较有挑战性

  • 注释非常的少
  • 宏定义非常的多
  • 函数名非常的长

源码搜索dispatch_get_main_queue:

dispatch_get_main_queue

dispatch_get_main_queue 是通过DISPATCH_GLOBAL_OBJECT返回的,是一个宏定义

  • DISPATCH_GLOBAL_OBJECT

DISPATCH_GLOBAL_OBJECT宏定义

  • 通过_dispatch_main_q参数搜索
dispatch_get_main_queue(void)
{
	return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}

_dispatch_main_q

  • 还可以通过主队列的dq_label搜索如下:

在这里插入图片描述 在源码中有dq_serialnum = 1,这是不是意味着可以作为主队列就是 串行队列的依据呢? 现在还不得而知,那么去看看串行队列底层是怎么实现的,或许可以找到答案!

  • 主队列的初始化是在dispatch_init()方法中?

在这里插入图片描述

  • dispatch_init()中成功找到了主队列初始化的地方,
  • 获取默认队列,
  • 并将主队列地址绑定到当前队列和主线程中

2.2 串行、并发队列分析

串行队列并发队列都通过dispatch_queue_create创建的,那么去搜索一下

dispatch_queue_create 通过搜索定位到dispatch_queue_create,在通过返回的是_dispatch_lane_create_with_target,再继续搜索

_dispatch_lane_create_with_target

  • 代码比较长,从返回值看,再推导

从返回值推导

  • _dispatch_object_alloc申请内存空间
  • _dispatch_queue_init构造函数初始化
  • 判断是否为并发队列,如果是,传入DISPATCH_QUEUE_WIDTH_MAX,否则传入1。也就是说,串行队列这里传入1,如果是并发队列,则传入DISPATCH_QUEUE_WIDTH_MAX

DISPATCH_QUEUE_WIDTH_MAX

  • dq进行设置,如dq_labeldq_priority

_dispatch_queue_init 在这里插入图片描述

  • 把前面的 width 传进来,赋值 dqf |= DQF_WIDTH(width)
  • DQF_WIDTH(width),也就是用来确定队列的类型,以此来区分串行队列并发队列

DISPATCH_QUEUE_SERIAL_NUMBER_INIT 其他参数vtabledqai,分别是什么呢?继续探索 在这里插入图片描述

  • dqai初始化

在开头有这么一句代码

// dqai 创建 - dqa传入的属性串行还是并行
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
  • _dispatch_queue_attr_to_info

_dispatch_queue_attr_to_info 在这里进行初始化了dqai,并判断dqa的类型,如果是并发队列,则设置并发队列true,否则默认为串行队列。在调用_dispatch_queue_initdq进行构造时,对队列类型进行了区分,也就是DQF_WIDTH(width)的传参,串行队列width=1,否则为并发队列

  • vtable
        const void *vtable; // - 设置类 类是通过宏定义拼接而成
        if (dqai.dqai_concurrent) {
                // OS_dispatch_##name##_class
                // OS_dispatch_queue_concurrent - 宏定义拼接类类型
                vtable = DISPATCH_VTABLE(queue_concurrent);
        } else {
                vtable = DISPATCH_VTABLE(queue_serial);
        }

vtable可以理解为是一个类,或者说构造队列的模板类,qai来区分队列的类型,根据队列的类型来初始化不同的vtableDISPATCH_VTABLE是一个宏定义的方法,全局搜索DISPATCH_VTABLE的定义

// DISPATCH_VTABLE定义
#define DISPATCH_VTABLE(name) DISPATCH_OBJC_CLASS(name)

// vtable symbols - 模板
#define DISPATCH_OBJC_CLASS(name) (&DISPATCH_CLASS_SYMBOL(name))

// 拼接形成类
#define DISPATCH_CLASS_SYMBOL(name) OS_dispatch_##name##_class

DISPATCH_VTABLE函数的传参根据不同的队列类型传参不一致。

并发队列:

queue_concurrent参数,最终拼接后,队列类型对应的类为:OS_dispatch_queue_concurrent

串行队列:

queue_serial参数,最终拼接后,队列类型对应的类为:OS_dispatch_queue_serial

所以vtable对应的就是队列的类型。通过拼接完成类的定义,这和我们在应用层使用的队列类型是一致的,如下图:

测试代码

2.3 全局队列分析

进入 dispatch_get_global_queueapi

dispatch_get_global_queue

  • 创建全局并发队列时可以传参数
  • 根据不同服务质量或者优先等级提供不同的并发队列。

通过全局队列的标识 在源码里面搜索🔍

全局队列集合

  • 系统会维护一个全局队列集合
  • 根据不同的服务质量或者优先等级提供不同的全局队列
  • 我们在开发工作中默认使用:dispatch_get_global_queue(0, 0)

专题系列文章

1.前知识

2. 基于OC语言探索iOS底层原理

3. 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

其它底层原理专题

1.底层原理相关专题

2.iOS相关专题

3.webApp相关专题

4.跨平台开发方案相关专题

5.阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

6.Android、HarmonyOS页面渲染专题

7.小程序页面渲染专题