前言
在上一篇文章iOS 多线程原理(1)详细介绍了进程、线程、多线程、队列以及线程调度的基础概念以及原理,今天,我们就根据实例来探究一下iOS底层多线程的实现原理。
学习重点
GCD基础概念以及使用
同异步执行不同类型队列任务区别
1. GCD介绍及使用
1.1 GCD基础概念
在上一篇文章iOS 多线程原理(上)末尾,列出了iOS关于多线程的技术解决方案,分别有pthread,NSThread,GCD以及NSOperation这四种,但是在iOS开发中我们使用最多的是GCD以及NSOperation,而NSOperation底层实际上也是由GCD来实现的,因此我们重点探究的应该是GCD技术,其官方介绍如下所示:
Grand Central Dispatch(GCD)是Apple开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在Mac OS X 10.6雪豹中首次推出,也可在iOS 4及以上版本使用。
而使用GCD的好处也是很多的,如下所示:
-
GCD可用于多核的并行运算。 -
GCD会自动利用更多的CPU内核(比如双核、四核)。 -
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。 -
程序员只需要告诉
GCD想要执行什么任务,不需要编写任何线程管理代码。
而在学习GCD之前,我们需要来了解一下GCD中的基础知识以及概念。
1.1.1 GCD中的任务及其执行方式
任务 :就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在GCD中是放在Block中的。执行任务有两种方式:同步执行和异步执行。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
同步执行(sync):
-
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
-
只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async):
-
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
-
可以在新的线程中执行任务,具备开启新线程的能力。
1.1.2 GCD中的队列
队列(Dispatch Queue) :这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
在GCD中有两种队列:串行队列和并发队列。两者都符合FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
串行队列(Serial Dispatch Queue):每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
两者区别如下图所示:
注意:串行队列只有在异步(dispatch_async)方法下才会开启线程执行任务,并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。
1.2 GCD的基本使用
1.2.1 队列的创建
创建队列主要使用dispatch_queue_create函数,如下图所示:
该函数需要传入两个参数:
-
参数1:label,表示队列的唯一标识符,用于DEBUG,可为空。队列的名称推荐使用应用程序ID这种逆序全程域名。 -
参数2:attr,用来识别是串行队列还是并发队列。传入DISPATCH_QUEUE_SERIAL表示串行队列,传入DISPATCH_QUEUE_CONCURRENT表示并发队列。
其返回值为dispatch_queue_t类型的变量。
除了可以使用dispatch_queue_create函数创建串行队列或并发队列,系统还默认提供了主队列以及全局队列的获取方式。
- 主队列(
Main Dispatch Queue):实际上是属于串行队列,但是所有放在主队列的任务仅会在主线程中执行,这是它与一般串行队列的区别,可以通过调用dispatch_get_main_queue()函数获取主队列,如下图所示:
可以发现这个函数不需要传参,并且返回一个disparch_queue_main_t类型的变量。
- 全局队列(
Global Dispatch Queue):实际上属于并发队列,可以通过调用dispatch_get_global_queue(intptr_t identifier, uintptr_t flags)函数来获取,如下图所示:
该函数需要传入两个参数:
-
参数
1:identifier,表示服务质量(优先级),一般传入DISPATCH_QUEUE_PRIORITY_DEFAULT即可。 -
参数
2:flags,暂时没用,传入0即可。
1.2.2 同异步函数
GCD提供了同步(dispatch_sync)执行队列任务的函数和异步(dispatch_async)执行队列任务两种方式。
同步函数如下图所示:
异步函数如下图所示:
这两个函数都需要传入两个参数,分别为:
-
参数1:queue,队列,也就是会同步执行queue队列中的任务。 -
参数2:block,闭包,也就是要执行的任务的代码块。
注意:同步函数所传入的闭包变量必须为非逃逸类型闭包。
2. 同异步执行不同类型队列任务区别
前面我们已经介绍了GCD中几种队列以及同异步函数及其作用,但是你可能还是不太熟悉,因此我们可以创建任务添加到这几种队列中,在同异步函数中分别执行,看看到底会有怎样的差别。
首先必须了解一点,在不创建新的队列以及线程之前,应用程序的代码默认(包括mian函数)是在主线程执行的,应用程序的main函数实际上也是添加到主队列中同步执行的,也就是说主队列实际上是在main函数调用之前就应经创建好了的,在main函数中打上断点,编译运行程序,在main函数调用栈中,可以看到如下图所示信息:
2.1 同异步执行主队列任务
2.1.1 主队列--任务同步执行
编写如下所示代码:
- (void)mainQueueSync {
//主队列实际上是一个特殊的串行队列,默认情况下,代码是在主线程的主队列中执行的
dispatch_queue_t mainQueue = dispatch_get_main_queue();
NSLog(@"1:主队列---同步执行---%@", [NSThread currentThread]);
for (int i = 0; i < 10; i++) {
dispatch_sync(mainQueue, ^{
NSLog(@"2:主队列:同步执行 -- %@", [NSThread currentThread]);
});
}
NSLog(@"3:主队列---同步执行---%@", [NSThread currentThread]);
}
在程序主线程中调用此方法,程序会崩溃报错,是由于循环等待而导致了死锁,如下图所示:
运行结果分析:
在主线程中调用mainQueueSync方法,事实上就是将mainQueueSync方法的代码块作为任务1添加到了主队列中同步执行,由于主队列是个串行队列,因此必须要执行先执行完任务1,才能执行其他任务,但是在mainQueueSync方法的执行过程中,又调用了同步函数dispatch_sync将其参数二闭包中的代码块作为任务2添加到了主队列中,而同步函数的作用就是要求主队列先将任务2执行完,否则不会继续执行mainQueueSync方法中的代码块(也就是任务1),因此就产生了死锁。
2.1.2 主队列--任务异步执行
编写如下所示代码:
- (void)mainQueueAsync {
//主队列实际上是一个特殊的串行队列,默认情况下,代码是在主线程的主队列中执行的
dispatch_queue_t mainQueue = dispatch_get_main_queue();
NSLog(@"1:主队列---异步代码块前---%@", [NSThread currentThread]);
for (int i = 0; i < 10; i++) {
dispatch_async(mainQueue, ^{
NSLog(@"2:主队列:异步执行 -- %@ -- %d", [NSThread currentThread], i);
});
}
sleep(2.0);
NSLog(@"3:主队列---异步代码块后---%@", [NSThread currentThread]);
}
在程序主线程中调用此方法,编译运行程序,控制台打印输出信息如下图所示:
运行结果分析:
在主线程中调用mainQueueAsync方法,实际上就是将mainQueueSync方法的代码块作为任务1添加到了主队列中同步执行,而由于主队列是串行队列,因此一定会将主队列中任务1执行完,才会接着顺序执行之后添加到主队列中的其他任务,在mainQueueSync方法for循环语句中调用了10次dispatch_async异步函数,也就是说添加了10个任务(block中的代码块)到主队列中,异步函数不需要主队列立即执行通过异步函数添加到主队列中的任务,因此这里不会产生死锁,因此任务1执行完之后,就会开始顺序执行主队列中其他任务,在block打印输出的线程都是主线程的原因是由于系统底层做了处理(主队列中的任务一定在主线程中执行),如果是其他自定义的串行队列,异步函数是会创建新的线程来执行队列中的任务的,这也是主队列与其他串行队列的区别所在。
2.2 同异步执行自定义串行队列任务
2.2.1 自定义串行队列--任务同步执行
编写如下所示代码:
- (void)serialQueueSync {
dispatch_queue_t serial = dispatch_queue_create("serial1", DISPATCH_QUEUE_SERIAL);
NSLog(@"1:主队列---同步函数---%@", [NSThread currentThread]);
for (int i = 0; i < 10; i++) {
dispatch_sync(serial, ^{
NSLog(@"2串行队列--同步函数:%@---%d", [NSThread currentThread], i);
});
}
sleep(2.0);
NSLog(@"3:主队列---同步函数---%@", [NSThread currentThread]);
}
在程序主线程中调用此方法,编译运行程序,控制台打印输出信息如下图所示:
运行结果分析:
在主线程中调用serialQueueSync方法,实际上就是将serialQueueSync方法的代码块作为任务1添加到了主队列中同步执行,执行for循环语句调用了10次dispatch_sync同步函数,由于是同步函数,不会创建新的线程,因此将参数二block中的代码块作为任务添加到所自定义的串行队列serial中后,会发生阻塞,等待此次串行队列serial中的任务在主线程中执行完毕后,才会执行下一次for循环语句,因此会顺序打印日志信息,最后执行3处的NSLog函数。
2.2.2 自定义串行队列--任务异步执行
编写如下所示代码:
- (void)serialQueueAsync {
dispatch_queue_t serial = dispatch_queue_create("serial1", DISPATCH_QUEUE_SERIAL);
NSLog(@"1:主队列---同步函数---%@", [NSThread currentThread]);
for (int i = 0; i < 10; i++) {
dispatch_async(serial, ^{
NSLog(@"2串行队列--异步函数:%@---%d", [NSThread currentThread], i);
});
}
sleep(2.0);
NSLog(@"3:主队列---同步函数---%@", [NSThread currentThread]);
}
在程序主线程中调用此方法,编译运行程序,控制台打印输出信息如下图所示:
运行结果分析:
在主线程中调用serialQueueAsync方法,实际上就是将serialQueueAsync方法的代码块作为任务1添加到了主队列中同步执行,执行for循环语句调用了10次dispatch_async异步函数,由于是异步函数,并且是串行队列,因此会创建一个新线程,将参数二block中的代码块作为任务添加到所自定义的串行队列serial中顺序执行,主线程不会发生阻塞,但是3处的NSLog函数的调用有可能发生在串行队列serial所有任务执行之前,也可能发生在串行队列serial任务执行的过程中,也有可能发生在串行队列serial所有任务执行之后,顺序是不确定的,但是串行队列中任务的执行顺序是确定的。
2.3 同异步执行自定义并发队列任务
2.3.1 自定义并发队列--任务同步执行
编写如下所示代码:
- (void)concurrentQueueSync {
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent1", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1:主队列---同步函数---%@", [NSThread currentThread]);
for (int i = 0; i < 10; i++) {
dispatch_sync(concurrentQueue, ^{
NSLog(@"2并发队列--同步函数:%@---%d", [NSThread currentThread], i);
});
}
sleep(2.0);
NSLog(@"3:主队列---同步函数---%@", [NSThread currentThread]);
}
在程序主线程中调用此方法,编译运行程序,控制台打印输出日志信息如下图所示:
运行结果分析:
在主线程中调用concurrentQueueSync方法,实际上就是将concurrentQueueSync方法的中的代码块作为任务1添加到了主队列中同步执行,执行for循环语句调用了10次dispatch_sync同步函数,由于是同步函数,不会创建新的线程,因此将参数二block中的代码块作为任务添加到所自定义的并发队列concurrentQueue中后,会发生阻塞,等待此次并发队列concurrentQueue中的任务在主线程中执行完毕后,才会执行下一次for循环语句,因此会顺序输出日志信息,最后执行3处的NSLog函数,与串行队列--任务同步执行的日志打印结果是一致的。
2.3.2 自定义并发队列--任务异步执行
编写如下所示代码:
- (void)concurrentQueueAsync {
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent1", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1:主队列---同步函数---%@", [NSThread currentThread]);
for (int i = 0; i < 10; i++) {
dispatch_async(concurrentQueue, ^{
NSLog(@"2并发队列--异步函数:%@---%d", [NSThread currentThread], i);
});
}
sleep(0.75);
NSLog(@"3:主队列---同步函数---%@", [NSThread currentThread]);
}
在程序主线程中调用此方法,编译运行程序,控制台打印输出信息如下图所示:
运行结果分析:
在主线程中调用concurrentQueueAsync方法,实际上就是将concurrentQueueAsync方法的中的代码块作为任务1添加到了主队列中同步执行,执行for循环语句调用了10次dispatch_async异步函数,由于是异步函数,并且是并发队列,因此会创建一个或多个新线程,将参数二block中的代码块作为任务添加到所自定义的并发队列concurrentQueue中并发执行(队列中任务执行完毕顺序不确定),主线程不会发生阻塞,但是3处的NSLog函数的调用有可能发生在并发队列concurrentQueue所有任务执行之前,也可能发生在并发队列concurrentQueue任务执行的过程中,也有可能发生在并发队列concurrentQueue所有任务执行之后,顺序是不确定的。
2.4 同异步执行全局队列任务
2.3.1 全局队列--任务同步执行
编写如下所示代码:
- (void)globalQueueSync {
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"1:主队列---同步函数---%@", [NSThread currentThread]);
for (int i = 0; i < 10; i++) {
dispatch_sync(globalQueue, ^{
NSLog(@"2:全局队列--同步函数:%@---%d", [NSThread currentThread], i);
});
}
sleep(2.0);
NSLog(@"3:主队列---同步函数---%@", [NSThread currentThread]);
}
在程序主线程中调用此方法,编译运行程序,控制台打印输出日志信息如下图所示:
运行结果分析:
与自定义并发队列--任务同步执行结果一致。
2.3.2 自定义并发队列--任务异步执行
编写如下所示代码:
- (void)globalQueueAsync {
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"1:主队列---同步函数---%@", [NSThread currentThread]);
for (int i = 0; i < 10; i++) {
dispatch_async(globalQueue, ^{
NSLog(@"2:全局队列--异步函数:%@---%d", [NSThread currentThread], i);
});
}
sleep(0.25);
NSLog(@"3:主队列---同步函数---%@", [NSThread currentThread]);
}
在程序主线程中调用此方法,编译运行程序,控制台打印输出信息如下图所示:
运行结果分析:
与自定义并发队列--任务异步执行结果一致。
3. GCD源码探究
我们所使用的GCD的接口函数实际上是属于libdispatch库的,这个库其实在我们探究应用程序的加载流程时就有接触过,你可以点击GCD源码进行下载阅读。
3.1 GCD队列底层结构
3.1.1 主队列的创建以及结构
在上述的代码使用过程中,我们已经对获取主队列dispatch_get_main_queue的函数的使用很熟悉了,现在就让我们看看这个函数底层源码是如何实现的,如下图所示:
在这个函数上面有详细的注释以及说明,大致如下:
-
函数返回返回绑定到主线程的默认队列(主队列)。
-
为了调用提交给主队列的闭包(任务),应用程序必须调用
dispatch_main()、NSApplicationMain()或者在主线程中使用CFRunLoop。 -
主队列是用来在应用程序上下文中与主线程以及主
runloop交互的。 -
主队列的作用不完全像一个常规的串行队列,因此,它有可能有不必要的副作用,当使用的不是UI应用程序(守护进程)时,应该避免使用主队列。
-
主队列是在main()函数调用之前为了主线程自动创建的。
而这个函数的实现代码是返回了DISPATCH_GLOBAL_OBJECT这个宏调用结果的返回值,这个宏的定义在libdispatch源码中有三处,如下所示:
//1
#define OS_OBJECT_BRIDGE __bridge
#define DISPATCH_GLOBAL_OBJECT(type, object) ((OS_OBJECT_BRIDGE type)&(object))
//2 static_cast是C++中的强制类型转换函数,不会做运行时的安全检查
#define DISPATCH_GLOBAL_OBJECT(type, object) (static_cast<type>(&(object)))
//3
#define DISPATCH_GLOBAL_OBJECT(type, object) ((type)&(object))
目前并不知道调用的是哪个宏,可能你也不清楚这三个宏的作用是什么,但是我们先来关注这个宏传入的两个参数
参数1:dispatch_queue_main_t,全局搜索,找到如下图所示代码:
可以看到这个dispatch_queue_main_t实际上是一个(struct dispatch_queue_static_s *类型)的结构体指针变量的类型,并且上面的注释还告诉我们如下信息:
主队列是一个众所周知的全局对象,它是在进程初始化期间代表主线程自动生成的,由dispatch_get_main_queue()返回,不能修改,dispatch_suspend()、dispatch_resume()、dispatch_set_context()等调用在主队列上使用时没有效果。
参数2:_dispatch_main_q,全局搜索,找到如下图所示代码:
以上代码是在queue.h文件中的声明,这段代码判断如果定义了__DISPATCH_BUILDING_DISPATCH__这个宏并且没有定义__OBJC__这个宏,_dispatch_main_q就声明为struct dispatch_queue_static_s类型结构体变量,否则就声明为struct dispatch_queue_s类型结构体变量。
以上代码是在init.c文件中的变量定义,这样我们就可以确定了dispatch_mian_q实际上是定义并初始化了的struct dispatch_queue_static_s结构体类型全局变量,而其成员变量dq_label的值实际上就是我们在应用程序中看到的主队列的标识符,而DQF_WIDTH这个宏中的传值实际上就决定了队列是串行还是并发(传1就是串行队列),而串行队列的标识信息就存储在其成员变量dq_atomic_flags中。
让我们再回到dispatch_get_main_queue函数中查看DISPATCH_GLOBAL_OBJECT这个宏的作用,你就会发现,实际上这个宏就是获取全局变量dispatch_mian_q的地址,也就是获取指向全局变量dispatch_mian_q的指针并返回这个指针,因此这个函数返回值类型才会是dispatch_queue_main_t(就是struct dispatch_queue_static_s *)。
但是你可能又会有些疑惑,为什么全局变量dispatch_main_q的类型为dispatch_queue_static_s,为什么在OC中接收全局变量dispatch_main_q的指针类型为dispatch_queue_t呢?带着这样的疑惑,我们来看看dispatch_queue_static_s结构体的代码,如下图所示:
你会发现其中定义了一个struct dispatch_lane_s类型的数组_as_dl,但是奇怪的是这个数组的长度为0,如果对C++编程语言不熟悉的小伙伴可能会很懵,不明白这样写的目的,其实这么写的目的就是单纯做一个标记,表示可以将struct dispatch_queue_static_s结构体类型的指针转换为sruct dispatch_lane_s结构体指针,因为如果你定义了一个struct dispatch_queue_static_s结构体类型的指针,假设命名为dqss1,那么其第一个成员变量_as_dl的地址实际上是与dqss1指针的值是一样的,由于_as_dl是一个struct dispatch_lane_s类型数组,那么也就可以表示dqss1指针的值是一个struct dispatch_lane_s类型数组第一个元素的首地址,既然dqss1指针的值也表示一个struct dispatch_lane_s类型元素的首地址,那么当然也可以称dqss1为一个指向struct dispatch_lane_s结构体类型的指针了,实际上,_as_dl被定义为一个长度为0的struct dispatch_lane_s类型的数组,而不定义为一个struct dispatch_lane_s类型的指针的原因就在于,系统会为指针类型变量开辟8字节的内存空间,但是不会为长度为0的数组开辟内存空间,虽然这两者均表示某个地址空间的地址。
但是在底层代码中,struct dispatch_queue_static_s结构体类型的指针之所以能够转换为struct dispatch_lane_s结构体类型指针的原因就在于,struct dispatch_queue_static_s结构体类型的成员变量包含了struct dispatch_lane_s结构体类型的所有变量,并且两者共有的成员变量类型以及顺序必须是一致的,我们可以通过以下的代码进行验证:
#include <iostream>
using namespace std;
typedef struct structDemoSuper {
int a;
float b;
char c;
}SDSuper;
typedef struct structDemo {
struct structDemoSuper sdSuper[0];
int a;
float b;
char c;
double d;
}SD;
typedef struct structDemo2 {
struct structDemoSuper *sdSuper;
int a;
float b;
char c;
double d;
}SD2;
typedef struct structDemoSub {
struct structDemoSuper sdSuper[0];
struct structDemo sdDemo[0];
int a;
float b;
char c;
double d;
}SDSub;
typedef struct structDemoSub2 {
struct structDemoSuper sdSuper[0];
struct structDemo sdDemo[0];
char a;
int c;
float b;
}SDSub2;
void test() {
SDSub *sub = (SDSub *)malloc(sizeof(SDSub));
sub->a = 10;
sub->b = 10.1;
sub->c = 'A';
sub->d = 3.1415926;
SD *sd1 = (SD *)sub;
cout<< "sd1->a = " << sd1->a << " sd1->b = " << sd1->b << " sd1->c = " << sd1->c << " sd1->d = " << sd1->d << endl;
cout<< "SD size = " << sizeof(SD) << " SD2 size = " << sizeof(SD2) << endl;
SDSub2 *sub2 = (SDSub2 *)malloc(sizeof(SDSub2));
sub2->c = 14;
sub2->a = 'B';
SD *sd2 = (SD *)sub2;
cout<< "sd2->a = " << sd2->a << " sd2->b = " << sd2->b << " sd2->c = " << sd2->c << " sd2->d = " << sd2->d << endl;
}
int main(int argc, const char * argv[]) {
test();
return 0;
}
编译运行程序,打印结果如下所示:
-
第一行,两个结构体的成员变量类型以及顺序是一致的,因此转换指针类型之后,两种指针类型变量成员变量的值是一样,这就类似与继承结构。
-
第二行,
SD中有一个数组大小为0的(struct structDemoSuper类型)结构体数组类型成员变量,SD2中有一个(struct structDemoSuper *类型)结构体指针,前者不占内存空间,后者占8字节内存空间。 -
第三行,
SDSub2结构体与SD结构体成员变量排列顺序以及数量不一致时,取出的成员变量的值就不一致。
而dispatch_queue_main_t类型能够强转为dispatch_queue_t类型的根本性原因就在于DISPATCH_LANE_CLASS_HEADER这个宏中,也就是说dispatch_queue_static_s结构体中的成员变量一定包含了dispatch_lane_s结构体中的成员变量,并且顺序一致,而dispatch_lane_s结构体中的成员变量一定包含了dispatch_queue_s结构体中的所有成员变量,并且顺序一致,这其实类似于OC中类的继承,子类中包含父类中定义的属性。
接着让我们来看看_dispatch_main_q全局变量中成员变量是如何赋值的,首先查看DISPATCH_GLOBAL_OBJECT_HEADER宏定义,如下所示:
#if OS_OBJECT_HAVE_OBJC1
#define DISPATCH_GLOBAL_OBJECT_HEADER(name) \
.do_vtable = DISPATCH_VTABLE(name), \
._objc_isa = DISPATCH_OBJC_CLASS(name), \
.do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT, \
.do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT
#else
#define DISPATCH_GLOBAL_OBJECT_HEADER(name) \
.do_vtable = DISPATCH_VTABLE(name), \
.do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT, \
.do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT
#endif
//DISPATCH_VTABLE定义与赋值
#if OS_OBJECT_HAVE_OBJC2
#define DISPATCH_VTABLE(name) DISPATCH_OBJC_CLASS(name)
#else
#define DISPATCH_VTABLE(name) &OS_OBJECT_EXTRA_VTABLE_SYMBOL(dispatch_##name)
#define DISPATCH_OBJC_CLASS(name) (&DISPATCH_CLASS_SYMBOL(name))
#define DISPATCH_CLASS_SYMBOL(name) OS_dispatch_##name##_class
//DISPATCH_OBJECT_GLOBAL_REFCNT定义与赋值
#define DISPATCH_OBJECT_GLOBAL_REFCNT _OS_OBJECT_GLOBAL_REFCNT
#define _OS_OBJECT_GLOBAL_REFCNT INT_MAX
在iOS端使用的应该是OS_OBJECT_HAVE_OBJC2,而不是OS_OBJECT_HAVE_OBJC1,因此可以得到dispatch_main_q前三个成员变量的值,如下所示:
.do_vtable = OS_dispatch_queue_main_class
.do_ref_cnt = INT_MAX
.do_xref_cnt = INT_MAX
其次是成员变量do_targetq的赋值,是通过宏_dispatch_get_default_queue赋值的,其定义如下所示:
#define _dispatch_get_default_queue(overcommit) \
_dispatch_root_queues[DISPATCH_ROOT_QUEUE_IDX_DEFAULT_QOS + \!!(overcommit)]._as_dq
其中_dispatch_root_queues实际上是定义的(struct dispatch_queue_global_s类型)结构体数组,也是一个全局变量,dispatch_queue_global_s结构体代码定义如下图所示:
其中DISPATCH_QUEUE_ROOT_CLASS_HEADER的定义如下所示:
#define DISPATCH_QUEUE_ROOT_CLASS_HEADER(x) \
struct dispatch_queue_s _as_dq[0]; \
DISPATCH_QUEUE_CLASS_HEADER(x, \
struct dispatch_object_s *volatile dq_items_tail); \
int volatile dgq_thread_pool_size; \
struct dispatch_object_s *volatile dq_items_head; \
int volatile dgq_pending
可以发现dispatch_queue_global_s结构体成员变量是包含了dispatch_queue_s结构体中所有的成员变量,并且另外多了dgq_thread_pool_size、dq_items_head以及dgq_pending三个成员变量,而_dispatch_root_queues数组的初始化代码如下所示:
可以发现_dispatch_root_queues数组中初始化了12个队列,并且队列的dq_serialnum是从4开始一直到15。
回到_dispatch_get_default_queue宏定义,搜索DISPATCH_ROOT_QUEUE_IDX_DEFAULT_QOS,可以发现它实际上是枚举类型,值为6,如下图所示:
因此(DISPATCH_ROOT_QUEUE_IDX_DEFAULT_QOS + !!(overcommit))值计算出来为7,也就是_dispath_roots_queues数组中第七个队列,如下图所示:
然后就是成员变量dq_state的赋值,是通过DISPATCH_QUEUE_STATE_INIT_VALUE(1) | DISPATCH_QUEUE_ROLE_BASE_ANON计算所得,这两个宏定义如下所示:
#define DISPATCH_QUEUE_STATE_INIT_VALUE(width) \
((DISPATCH_QUEUE_WIDTH_FULL - (width)) << DISPATCH_QUEUE_WIDTH_SHIFT)
#define DISPATCH_QUEUE_WIDTH_FULL 0x1000ull
#define DISPATCH_QUEUE_WIDTH_SHIFT 41
#define DISPATCH_QUEUE_ROLE_BASE_ANON 0x0000001000000000ull
因此计算dq_state的值((0x1000 - 1 << 41) | 0x0000001000000000)为0x1FFE0010000000
最后就是dq_atomic_flags成员变量的值,通过DQF_THREAD_BOUND(0x00040000) | DQF_WIDTH(1)计算为0x00040001,如果在项目工程中使用po命令打印主队列的信息,你会发现打印信息是与上述信息吻合的,如下图所示:
3.1.2 自定义队列的创建以及结构
首先来查看dispatch_queue_create函数底层代码,如下图所示:
在这个函数中实际上调用了_dispatch_lane_create_with_target函数,其代码如下所示:
这个函数的代码有一百多行,其实是蛮不好分析的,这个函数的返回值类型为dispatch_queue_t,那么dispatch_queue_t又是什么类型的变量呢,其定义如下所示:
DISPATCH_DECL(dispatch_queue);
//DISPATCH_DECL宏的定义
#define DISPATCH_DECL(name) \
typedef struct name##_s : public dispatch_object_s {} *name##_t
由以上代码可知dispatch_queue_t实际上是(struct dispatch_queue_s *)结构体指针类型,,并且这个宏定义表示dispatch_queue_s结构体实际上继承自dispatch_object_s,以一下是关于dispatch_queue_t的官方注释:
-
抽象来说:分派队列调用提交给它们的工作项
-
discussion:
-
分派队列有多重形式,最常见的是串行分派队列(
dispatch_queue_serial_t)。 -
系统管理一个线程池,这些线程处理分配队列并调用提交给它们的工作项。
-
从概念上将,分配队列可能有它自己的执行线程,队列之间的交互是高度异步的。
-
通过调用
dispatch_retain()和dispatch_release()对队列进行引用计数,提交到队列的工作项也持有对队列的引用,直到这些工作项完成为止。一旦对队列的所有引用都被释放,队列将被系统释放。
而dispatch_queue_s结构体定义如下图所示:
而其中的宏DISPATCH_QUEUE_CLASS_HEADER定义如下所示:
//此时:x为queue,__pointer_sized_field__为指针__dq_opaque1
DISPATCH_QUEUE_CLASS_HEADER(x, __pointer_sized_field__) \
_DISPATCH_QUEUE_CLASS_HEADER(x, __pointer_sized_field__); \
/* LP64 global queue cacheline boundary */ \
unsigned long dq_serialnum; \
const char *dq_label; \
DISPATCH_UNION_LE(uint32_t volatile dq_atomic_flags, \
const uint16_t dq_width, \
const uint16_t __dq_opaque2 \
); \
dispatch_priority_t dq_priority; \
union { \
struct dispatch_queue_specific_head_s *dq_specific_head; \
struct dispatch_source_refs_s *ds_refs; \
struct dispatch_timer_source_refs_s *ds_timer_refs; \
struct dispatch_mach_recv_refs_s *dm_recv_refs; \
struct dispatch_channel_callbacks_s const *dch_callbacks; \
}; \
int volatile dq_sref_cnt
也就是说dispatch_queue_s结构体中包含了dq_serialnum(队列序号),dq_label(也就是传入的参数lable,标识符),dq_priority(队列的优先级)等专属成员变量,而其中_DISPATCH_QUEUE_CLASS_HEADER宏的定义如下所示:
//此时:x为queue,__pointer_sized_field__为指针__dq_opaque1
#define _DISPATCH_QUEUE_CLASS_HEADER(x, __pointer_sized_field__) \
DISPATCH_OBJECT_HEADER(x); \
__pointer_sized_field__; \
DISPATCH_UNION_LE(uint64_t volatile dq_state, \
dispatch_lock dq_state_lock, \
uint32_t dq_state_bits \
)
#endif
其中,DISPATCH_OBJECT_HEADER宏的定义如下所示:
//x为queue
#define DISPATCH_OBJECT_HEADER(x) \
struct dispatch_object_s _as_do[0]; \
_DISPATCH_OBJECT_HEADER(x)
这个宏中还包含了_DISPATCH_OBJECT_HEADER这个宏,其实在dispatch_object_s结构体定义中,也包含这个宏,如下图所示:
_DISPATCH_OBJECT_HEADER宏的定义如下所示:
//x为queue
#define _DISPATCH_OBJECT_HEADER(x) \
struct _os_object_s _as_os_obj[0]; \
OS_OBJECT_STRUCT_HEADER(dispatch_##x); \
struct dispatch_##x##_s *volatile do_next; \
struct dispatch_queue_s *do_targetq; \
void *do_ctxt; \
union { \
dispatch_function_t DISPATCH_FUNCTION_POINTER do_finalizer; \
void *do_introspection_ctxt; \
}
按照之前的规律,那么结构体_os_object_s定义中应该也包含OS_OBJECT_STRUCT_HEADER这个宏,我们查看这个结构体的定义代码如下所示:
发现其实结构体_os_object_s中包含了_OS_OBJECT_HEADER这个宏,这个宏定义如下所示:
#define _OS_OBJECT_HEADER(isa, ref_cnt, xref_cnt) \
isa; /* must be pointer-sized and use __ptrauth_objc_isa_pointer */ \
int volatile ref_cnt; \
int volatile xref_cnt
而OS_OBJECT_STRUCT_HEADER宏定义如下所示:
//x为dispatch_queue
#define OS_OBJECT_STRUCT_HEADER(x) \
_OS_OBJECT_HEADER(\
const struct x##_vtable_s *__ptrauth_objc_isa_pointer do_vtable, \
do_ref_cnt, \
do_xref_cnt)
#endif
可以发现OS_OBJECT_STRUCT_HEADER宏其实只是对_OS_OBJECT_HEADER宏进行了一次包装。
因此对dispatch_queue_s结构体中成员变量进行还原大致将得到如下结果:
struct dispatch_queue_s {
struct dispatch_object_s _as_do[0];
struct _os_object_s _as_os_obj[0];
const struct dispatch_queue_vtable_s *__ptrauth_objc_isa_pointer do_vtable;
int volatile do_ref_cnt;
int volatile do_xref_cnt;
struct dispatch_queue_s *volatile do_next;
struct dispatch_queue_s *do_targetq;
void *do_ctxt;
union {
dispatch_function_t DISPATCH_FUNCTION_POINTER do_finalizer;
void *do_introspection_ctxt;
}
void *__dq_opaque1;
union {
uint64_t volatile dq_state;
struct {
dispatch_lock dq_state_lock;
uint32_t dq_state_bits;
};
};
/* LP64 global queue cacheline boundary */ \
unsigned long dq_serialnum;
const char *dq_label;
union {
uint32_t volatile dq_atomic_flags;
struct {
const uint16_t dq_width; // 队列宽度(串行队列为1,并行队列大于1)
const uint16_t __dq_opaque2;
};
};
dispatch_priority_t dq_priority;
union { \
struct dispatch_queue_specific_head_s *dq_specific_head;
struct dispatch_source_refs_s *ds_refs;
struct dispatch_timer_source_refs_s *ds_timer_refs;
struct dispatch_mach_recv_refs_s *dm_recv_refs;
struct dispatch_channel_callbacks_s const *dch_callbacks;
};
int volatile dq_sref_cnt
/* 32bit hole on LP64 */
} DISPATCH_ATOMIC64_ALIGN;
画出结构图,大致如下图所示:
其次,我们先来看看其return语句,如下图所示:
可以看到return语句中又调用了_dispatch_trace_queue_create函数,但是却传入了一个dq变量作为参数,因此在_dispatch_lane_create_with_target函数中我们关注的重点,应该是dq是如何创建的,如下图所示:
而dispatch_lane_t则是(struct dispatch_lane_s *类型的)结构体指针类型,dispatch_lane_t与struct dispatch_lane_s定义如下图所示:
dispatch_lane_s结构体中的宏DISPATCH_LANE_CLASS_HEADER定义如下所示:
//x为lane
#define DISPATCH_LANE_CLASS_HEADER(x) \
struct dispatch_queue_s _as_dq[0]; \
DISPATCH_QUEUE_CLASS_HEADER(x, \
struct dispatch_object_s *volatile dq_items_tail); \
dispatch_unfair_lock_s dq_sidelock; \
struct dispatch_object_s *volatile dq_items_head; \
uint32_t dq_side_suspend_cnt
DISPATCH_QUEUE_CLASS_HEADER这个宏我们在上面已经见过了,因此我们还原一下dispatch_lane_s结构体,大致如下:
typedef struct dispatch_lane_s {
struct dispatch_queue_s _as_dq[0];
struct dispatch_object_s _as_do[0];
struct _os_object_s _as_os_obj[0];
const struct dispatch_lane_vtable_s *__ptrauth_objc_isa_pointer do_vtable;
int volatile do_ref_cnt;
int volatile do_xref_cnt);
struct dispatch_lane_s *volatile do_next;
struct dispatch_queue_s *do_targetq;
void *do_ctxt;
union {
dispatch_function_t DISPATCH_FUNCTION_POINTER do_finalizer;
void *do_introspection_ctxt;
}
struct dispatch_object_s *volatile dq_items_tail;
union {
uint64_t volatile dq_state;
struct {
dispatch_lock dq_state_lock;
uint32_t dq_state_bits;
};
};
/* LP64 global queue cacheline boundary */
unsigned long dq_serialnum;
const char *dq_label;
union {
uint32_t volatile dq_atomic_flags;
struct {
const uint16_t dq_width; // 队列宽度(串行队列为1,并行队列大于1)
const uint16_t __dq_opaque2;
};
};
dispatch_priority_t dq_priority;
union {
struct dispatch_queue_specific_head_s *dq_specific_head;
struct dispatch_source_refs_s *ds_refs;
struct dispatch_timer_source_refs_s *ds_timer_refs;
struct dispatch_mach_recv_refs_s *dm_recv_refs;
struct dispatch_channel_callbacks_s const *dch_callbacks;
};
int volatile dq_sref_cnt
dispatch_unfair_lock_s dq_sidelock;
struct dispatch_object_s *volatile dq_items_head;
uint32_t dq_side_suspend_cnt
/* 32bit hole on LP64 */
} DISPATCH_ATOMIC64_ALIGN *dispatch_lane_t;
可以发现的是dispatch_lane_s这个结构体只是比dispatch_queue_s结构体多了dq_sidelock、dq_items_head以及dq_side_suspend_cnt三个成员变量而已。
熟悉了dispatch_lane_s这个结构体类型之后,我们再来看看在_dispatch_lane_create_with_target函数中是如何给其成员变量初始化的代码如下所示:
这个函数中主要传入了4个参数,第一个参数是dispatch_lane_t类型变量dq,第二个参数dqf(dispatch_queue_flags_t 枚举类型),第三个参数会根据队列类型传入不同的值(如果是串行队列就传入1,如果是并发队列就是DISPATCH_QUEUE_WIDTH_MAX宏的值,为4094),第三个参数会根据队列是否活跃传入不同的值(不活跃值为DISPATCH_QUEUE_INACTIVE,活跃值为0,其相关枚举值如下图所示)
而_dispatch_queue_init函数代码如下图所示:
在这个函数中,可以发现参数1是一个dispatch_queue_class_t类型的变量,而dispatch_queue_class_t实际上是一个联合体,它是分派队列集群类,可以为任何dispatch_queue_t类型,如下图所示:
可以发现其中定义了多种类型的结构体指针,其中就包含了dq的类型(dispatch_lane_t,也就是struct dispatch_lane_s *),因此你可以将dq通过传参直接转换为dispatch_queue_class_t联合体类型变量,紧接着在这个函数中dq_state局部变量是通过宏DISPATCH_QUEUE_STATE_INIT_VALUE以及参数width获取得到的,这个宏定义如下所示:
#define DISPATCH_QUEUE_STATE_INIT_VALUE(width) \
((DISPATCH_QUEUE_WIDTH_FULL - (width)) << DISPATCH_QUEUE_WIDTH_SHIFT)
#define DISPATCH_QUEUE_WIDTH_FULL 0x1000ull
#define DISPATCH_QUEUE_WIDTH_SHIFT 41
通过计算可得,如果是串行队列dq_state为(0x1000ull - 1 << 41)0x1FFE0000000000,如果是并发队列dq_state为(0x1000ull - 4094 << 41)0x40000000000。
然后在经过dq_state |= initial_state_bits的计算可以得到,
当串行队列为活跃状态dq_state为(0x1FFE0000000000 | 0x0180000000000000)0x19FFE0000000000,不活跃dq_state为(0x1FFE0000000000 | 0)0,当并发队列为活跃状态dq_state为(0x40000000000 | 0x0180000000000000)0x180040000000000,活跃dq_state为(0x40000000000 | 0)为0,然后将dq_state局部变量的值存储到dq的成员变量dq_state中。
dq成员变量do_next(struct dispatch_x_s *结构体指针类型,x根据类型做判断,dq是dispatch_lane_s,则x为lane)通过DISPATCH_OBJECT_LISTLESS宏赋值,如下所示:
以上的代码意思就是dq成员变量do_next初始值不能赋值为NULL,而是赋值为一个野指针。
接着再来看看dq成员变量dq_atomic_flags(uint32_t类型)的赋值(os_atomic_store2o其实封装了C++底层函数atomic_store_explicit,实际上就是将dqf的值存储到dq成员变量dq_atomic_flags中),而dqf |= DQF_WIDTH(width)这句代码的意思就是将队列的宽度width(实际上也是表示队列的类型,并发队列为0xFFE,串行队列为0x1),存储到dqf的低16位,而dqf的枚举类型dispatch_queue_flags的信息只是存储在高16位中,如下图所示:
最后再来看看dq成员变量dq_serialnum如何赋值的,调用了宏os_atomic_inc_orig进行赋值,传入了_dispatch_queue_serial_numbers全局变量的地址,具体如下所示:
// skip zero
// 1 - main_q
// 2 - mgr_q
// 3 - mgr_root_q
// 4,5,6,7,8,9,10,11,12,13,14,15 - global queues
// 17 - workloop_fallback_q
// we use 'xadd' on Intel, so the initial value == next assigned
#define DISPATCH_QUEUE_SERIAL_NUMBER_INIT 17
unsigned long volatile _dispatch_queue_serial_numbers = DISPATCH_QUEUE_SERIAL_NUMBER_INIT;
#define os_atomic_inc_orig(p, m) \
os_atomic_add_orig((p), 1, m)
#define os_atomic_add_orig(p, v, m) \
_os_atomic_c11_op_orig((p), (v), m, add, +)
#define _os_atomic_c11_op_orig(p, v, m, o, op) \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), v, \
memory_order_##m)
将宏进行拼接替换,os_atomic_inc_orig实际上是调用了C++底层函数atomic_fetch_add_explicit,这个函数用在此处的作用是:原子替换指向的值_dispatch_queue_serial_numbers和添加1到旧值的结果_dispatch_queue_serial_numbers,并返回_dispatch_queue_serial_numbers先前保存的值,就是先将_dispatch_queue_serial_numbers的值(初始值为17)赋值给dq_serialnum,然后原子替换_dispatch_queue_serial_numbers的值为18,根据以上注释我们也可以了解到,实际上前16位的序号都是用来标识特定队列的(例如:全局变量主队列dispatch_main_q成员变量dq_serialnum的值就是1),而自定义的队列是通过调用dispatchu_queue_create()函数创建的队列,dq_serialnum的值是从17开始,依次递增的。
回到_dispatch_lane_create_with_target函数,查看接下来的代码,如下图所示:
-
方框1:就是将参数label(队列标识符)赋值给dq的成员变量dq_label。 -
方框2:设置dq成员变量dq_priority(优先级)的值,局部变量dqai的创建如下图所示:
_dispatch_queue_attr_to_info函数代码如下图所示:
首先我们应该探究dqa传入的值是什么,其实是dispatch_queue_create函数中的第二个参数(dispatch_queue_attr_t 类型)attr,如果要创建的是串行队列传入的就是DISPATCH_QUEUE_SERIAL,如果创建的是并发队列传入的就是DISPATCH_QUEUE_CONCURRENT,这两个宏的定义如下所示:
//可用于创建按FIFO顺序连续调用块的分派队列的属性。
#define DISPATCH_QUEUE_SERIAL NULL
//一个属性,可用于创建可以并发调用块的分派队列,并支持使用栅栏函数API提交的栅栏块。
#define DISPATCH_QUEUE_CONCURRENT \
DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
_dispatch_queue_attr_concurrent)
可以发现串行队列参数2传入的其实就是NULL,而并发队列传入的是dispatch_queue_attr_t类型的变量,其实是指向_dispatch_queue_attr_concurrent全局变量地址的指针(DISPATCH_GLOBAL_OBJECT宏定义在前面我们已经接触过了),而全局变量_dispatch_queue_attr_concurrent的声明以及初始化代码如下所示:
//声明
DISPATCH_EXPORT
struct dispatch_queue_attr_s _dispatch_queue_attr_concurrent;
//初始化赋值
struct dispatch_queue_attr_s _dispatch_queue_attr_concurrent = {
DISPATCH_GLOBAL_OBJECT_HEADER(queue_attr),
};
首先来看看dispatch_queue_attr_s结构体是如何定义的,如下所示:
其中OS_OBJECT_STRUCT_HEADER宏前面已经见过了,其实就是相当于dispatch_queue_attr_s结构体继承了_os_object_s结构体,替换这个宏之后dispatch_queue_attr_s结构体定义如下所示:
struct dispatch_queue_attr_s {
const struct dispatch_queue_attr_vtable_s *__ptrauth_objc_isa_pointer do_vtable;
int volatile do_ref_cnt;
int volatile do_xref_cnt;
};
然后再来看看全局变量_dispatch_queue_attr_concurrent是如何初始化的,根据DISPATCH_GLOBAL_OBJECT_HEADER宏定义(前面遇到过了)的展开,得到_dispatch_queue_attr_concurrent成员变量值,如下所示:
struct dispatch_queue_attr_s _dispatch_queue_attr_concurrent = {
.do_vtable = OS_dispatch_queue_attr_class
.do_ref_cnt = INT_MAX,
.do_xref_cnt = INT_MAX,
};
然后再来看看全局变量_dispatch_queue_attrs的定义,如下图所示:
其实这里初始化了(DISPATCH_QUEUE_ATTR_COUNT - 1)4032大小的dispatch_queue_attr_s数组类型全局变量_dispatch_queue_attrs,并且根据注释可知,_dispatch_queue_attr_concurrent的别名就是_dispatch_queue_attrs[0],这里也保证了数组中第一个元素的成员变量与_dispatch_queue_attr_concurrent全局变量的成员变量值是匹配的,其中_dispatch_queue_attr_concurrent宏的定义如下所示:
//也就是 3 * 3 * 7 * 16 * 2 * 2 = 4032
#define DISPATCH_QUEUE_ATTR_COUNT ( \
DISPATCH_QUEUE_ATTR_OVERCOMMIT_COUNT * \
DISPATCH_QUEUE_ATTR_AUTORELEASE_FREQUENCY_COUNT * \
DISPATCH_QUEUE_ATTR_QOS_COUNT * \
DISPATCH_QUEUE_ATTR_PRIO_COUNT * \
DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT * \
DISPATCH_QUEUE_ATTR_INACTIVE_COUNT )
再回到_dispatch_queue_attr_to_info函数,接着分析代码,
红框1:如果创建的是串行队列,dqa为NULL,那么直接返回初始值都为0的dispatch_queue_attr_info_t类型结构体变量dqai,此结构体定义如下:
typedef uint32_t dispatch_qos_t;
typedef struct dispatch_queue_attr_info_s {
dispatch_qos_t dqai_qos : 8;
int dqai_relpri : 8;
uint16_t dqai_overcommit:2;
uint16_t dqai_autorelease_frequency:2;
uint16_t dqai_concurrent:1;
uint16_t dqai_inactive:1;
} dispatch_queue_attr_info_t;
这个结构体中使用了位域,实际上定义的结构体变量只有4字节大小。
-
红框2:意思是如果定义了DISPATCH_VARIANT_STATIC这个宏,并且dqa是指向全局变量_dispatch_queue_attr_concurrent的指针,将adqi的属性dqai_concurrent赋值为true(表示全局队列)并返回。 -
红框3:如果daq指针不指向_dispatch_queue_attrs中数组任意元素,那么使用memcmp比较dqa与数组第一个元素地址中相应字节成员变量的值,如果相等,就将daq指向数组的地址,如果不相等就直接调用DISPATCH_CLIENT_CRASH宏报错。 -
红框4:获取dqa在数组中的索引位置,并给dqai中各个成员变量赋值,计算规则就如注释所示,但是可以肯定的是idx为0时(也就是当创建的是全局队列时),dqai成员变量dqai_concurrent计算就为true,而其他成员变量的值都为0,最后返回dqai。
最后再回到_dispatch_lane_create_with_target函数继续分析代码:
获取到的qos此时值应为DISPATCH_QOS_UNSPECIFIED,因此不会为qos重新赋值,接着查看下面流程,如下图所示:
红框3处的代码会执行,因为不管是自定义创建串行队列还是并发队列,此时overcommit都为0,而_dispatch_queue_attr_overcommit_t枚举值中各个枚举值如下图所示:
因此执行完红框3处的代码之后,如果创建的是串行队列overcommit值为1(_dispatch_queue_attr_overcommit_enabled),如果是并发队列,overcommit值为2(_dispatch_queue_attr_overcommit_disabled),查看接下来的代码,如下图所示:
由于tq为NULL因此会执行这个分支代码,这里调用了_dispatch_get_root_queue函数,并传入参数,此时qos为DISPATCH_QOS_UNSPECIFIED,则参数1传入的值为DISPATCH_QOS_DEFAULT(也就是4),参数2则根据overcommit的值进行判断,如果是串行队列,则为true,如果为并发队列则为false,_dispatch_get_root_queue函数代码如下所示:
_dispatch_root_queues在前面已经介绍过了,它是一个有12个root队列的数组,调用过这个函数后,如果是串行队列,tq将指向_dispatch_root_queues数组中第(2 * 3 + 1)7个队列元素的地址,如下图所示:
如果是并发队列,tq将指向_dispatch_root_queues数组中第(2 * 3 + 0)6个队列元素的地址,如下图所示:
继续分析代码,如下图所示:
-
红框1:不会执行第二个if语句中的代码块。 -
红框2:先来查看DISPATCH_VTABLE相关宏定义,如下所示:
#define DISPATCH_VTABLE(name) DISPATCH_OBJC_CLASS(name)
#define DISPATCH_OBJC_CLASS(name) (&DISPATCH_CLASS_SYMBOL(name))
#define DISPATCH_CLASS_SYMBOL(name) _dispatch_##name##_vtable
因此如果是串行队列,vtable指向类符号_dispatch_queue_serial_vtable的地址,如果是并发队列,vtable指向类符号_dispatch_queue_concurrent_vtable的地址。
红框3:dqai的成员变量dqai_autorelease_frequency(自动释放频率)是dispatch_autorelease_frequency枚举类型,其定义如下所示:
-
DISPATCH_AUTORELEASE_FREQUENCY_INHERIT:此枚举类型意思是,此调度队列从其目标队列继承行为,这是手动创建的队列的默认行为。 -
DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM:使用此枚举值的调度队列,在异步提交给它的每个块的执行周围推并弹出一个自动释放池。 -
DISPATCH_AUTORELEASE_FREQUENCY_NEVER:使用此枚举值的调度队列不会在异步提交的块的执行周围设置单独的自动释放池。这是全局并发队列的行为。
因此这个switch语句中的分支也不会执行,最后再来分析最后一部分的代码。
红框1:使用宏_dispatch_priority_make设置队列优先级,此宏相关定义如下所示:
#define _dispatch_priority_make(qos, relpri) \
(qos ? ((((qos) << DISPATCH_PRIORITY_QOS_SHIFT) & DISPATCH_PRIORITY_QOS_MASK) | \
((dispatch_priority_t)(relpri - 1) & DISPATCH_PRIORITY_RELPRI_MASK)) : 0)
#define DISPATCH_PRIORITY_QOS_SHIFT 8
#define DISPATCH_PRIORITY_QOS_MASK ((dispatch_priority_t)0x00000f00)
#define DISPATCH_PRIORITY_RELPRI_MASK ((dispatch_priority_t)0x000000ff)
根据以上宏的计算(0 ? ((((0) << 8) & 0x00000f00) | ((0 - 1) & 0x000000ff)) : 0)为0,因此无论创建的是串行队列还是并行队列,队列优先级都为0(也就是DISPATCH_QUEUE_PRIORITY_DEFAULT)
-
红框2:如果是串行队列,还会将其优先级|=DISPATCH_PRIORITY_FLAG_OVERCOMMIT(0x80000000)。 -
红框3:!dqai.dqai_inactive条件成立,就从target queue继承优先级以及wlh。 -
红框4:创建的队列持有tq(也就是从12个root队列中获取到的对应队列)。
3.1.3 全局队列创建以及结构
首先先来查看一下dispatch_get_global_queue函数底层源码,如下所示:
首先第一个参数传入的是并发队列的优先级,第二个参数是一个flags标识。
红框1:做了一个判断如果(flags & ~(unsigned long)DISPATCH_QUEUE_OVERCOMMIT)条件成立,则返回DISPATCH_BAD_INPUT,其定义如下所示:
/*!
* @enum dispatch_queue_flags_t
*
* @constant DISPATCH_QUEUE_OVERCOMMIT
* The queue will create a new thread for invoking blocks, regardless of how
* busy the computer is.
*/
enum {
DISPATCH_QUEUE_OVERCOMMIT = 0x2ull,
};
#define DISPATCH_BAD_INPUT ((void *_Nonnull)0)
首先来看看那DISPATCH_BAD_INPUT宏的定义,可以发现它的值为NULL,DISPATCH_QUEUE_OVERCOMMIT这个枚举类型的值为0x2ull,则~(unsigned long)0x2ull的值为0xfffffffffffffffd,因此当(flags & 0xfffffffffffffffd)为真(不等于0)时会返回NULL,所以dispatch_get_global_queue中参数2的值一般都传0,否则会返回NULL。
红框2:首先调用_dispatch_qos_from_queue_priority函数获取priority对应的qos,其代码如下所示:
通过以上代码可以发现,队列的优先级与服务质量是一一对应的
但是当你传入的priority的值不包含队列优先级中对应宏的值,就会将priority强制转换为qos_class_t类型作为参数传入_dispatch_qos_from_qos_class函数,其代码如下图所示:
如果传入的property不为qos_class_t中的枚举类型的话,qos将被赋值为DISPATCH_QOS_UNSPECIFIED(值为0),然后接着判断HAVE_PTHREAD_WORKQUEUE_QOS(是否包含pthread工作队列服务质量)是否为假,为假,如果qos是QOS_CLASS_MAINTENANCE,则将其赋值为DISPATCH_QOS_BACKGROUND,如果qos是QOS_CLASS_USER_INTERACTIVE,则将其赋值为DISPATCH_QOS_USER_INITIATED。
-
红框3:如果qos是DISPATCH_QOS_UNSPECIFIED,就返回NULL,也就是说传入的priority如果不是DISPATCH_QUEUE_PRIORITY_BACKGROUND(值为INT16_MIN)、DISPATCH_QUEUE_PRIORITY_HIGH(值为2)、DISPATCH_QUEUE_PRIORITY_DEFAULT(值为0)、DISPATCH_QUEUE_PRIORITY_LOW(值为-2)、DISPATCH_QUEUE_PRIORITY_NON_INTERACTIVE(值为INT8_MIN)、QOS_CLASS_USER_INTERACTIVE(值为0x21)、QOS_CLASS_USER_INITIATED(值为0x19)、QOS_CLASS_DEFAULT(值为0x15)、QOS_CLASS_UTILITY(值为0x11)、QOS_CLASS_BACKGROUND(值为0x09)、QOS_CLASS_MAINTENANCE(值为0x05)其中类型的话,qos的值就为DISPATCH_QOS_UNSPECIFIED。 -
红框4:根据qos的值以及(flags&DISPATCH_QUEUE_OVERCOMMIT,为0)的值返回根队列数组中相应根队列,_dispatch_get_root_queue函数如下所示:
调用这个函数时,传入的参数1``qos的值(这里只讨论队列优先级)可能为DISPATCH_QOS_BACKGROUND、DISPATCH_QOS_UTILITY、DISPATCH_QOS_DEFAULT或DISPATCH_QOS_USER_INITIATED,而参数2``overcommit的值此时只能为0,因此获取到的队列的索引将有如下4种情况
- 当
priority为DISPATCH_QUEUE_PRIORITY_BACKGROUND,qos为DISPATCH_QOS_BACKGROUND(值为2),获取到的根队列的索引为(2 *(2 - 1)- 0)2,如下图所示:
- 当
priority为DISPATCH_QUEUE_PRIORITY_LOW,qos为DISPATCH_QOS_UTILITY(值为3),获取到的根队列的索引为(2 *(3 - 1)- 0)4,如下图所示:
- 当
priority为DISPATCH_QUEUE_PRIORITY_DEFAULT,qos为DISPATCH_QOS_DEFAULT(值为4),获取到的根队列的索引为(2 *(4 - 1)- 0)6,如下图所示:
- 当
priority为DISPATCH_QUEUE_PRIORITY_HIGH,qos为DISPATCH_QOS_USER_INITIATED(值为5),获取到的根队列的索引为(2 * (5 - 1)- 0)8,如下图所示:
3.1.4 其他队列
除了上面的12个根队列以及主队列之外,其实系统还创建了另外两个管理队列以及一个默认的运行循环根队列,我们来稍微分析一下这三个队列。
3.1.4.1 _dispatch_mgr_root_queue队列
其初始化代码如下图所示:
其类型为dispatch_queue_global_s类型(前面分析过了),其dq_label为com.apple.root.libdispatch-manager,dq_atomic_flags中宽度为0xFFF,队列优先级dq_priority为(DISPATCH_PRIORITY_FLAG_MANAGER(0x02000000) |
DISPATCH_PRIORITY_SATURATED_OVERRIDE(0x000f0000))0x020f0000,可以发现其优先级是很高的,dq_serialnum为3。
3.1.4.2 _dispatch_mgr_q队列
其初始化代码如下图所示:
其类型为dispatch_queue_static_s类型(前面分析过了),与主队列类型一致,其do_targetq为_dispatch_mgr_root_queue队列,dq_label为com.apple.libdispatch-manager,队列dq_atomic_flags中宽度与主队列一样为1,
dq_priority也与_dispatch_mgr_root_queue队列优先级一样,dq_serialnum为2。
3.1.4.3 _dispatch_custom_workloop_root_queue队列
其初始化代码如下图所示:
其类型为dispatch_queue_global_s类型(前面分析过了),是一个全局队列类型,dq_label为com.apple.root.workloop-custom,队列dq_atomic_flags中宽度为DISPATCH_QUEUE_WIDTH_POOL(0x1000ull - 1,0xFFF),
dq_priority根据计算为0x040F4000,dq_serialnum为DISPATCH_QUEUE_SERIAL_NUMBER_WLF,也就是16。
3.1.5 队列总结
-
主队列、自定义串行队列、自定义并发队列它们的
target queue都有指定,主队列与自定义串行队列的target queue都为根队列数组中索引为7的队列,自定义并发队列的target queue为根队列数组中索引为6的队列,但是全局队列没有target queue。(其实指定target queue的目的就在于可以将队列中的任务交给所指的的target queue执行,而全局队列没有target queue,因此它只能自己执行自己的任务)。 -
主队列、自定义队列、自定义并发队列底层类型实际上都为
dispatch_lane_s(主队列为dispatch_queue_static_s,只是相当于起了个别名),而全局队列底层类型为dispatch_queue_global_s(与dispath_lane_s同级,都包含了dispatch_queue_s中所有的成员变量)。 -
主队列以及自定义队列的
target queue都有overcommit标识,而自定义并发队列以及全局队列都没有overcommit标识。(有overcommit标识的队列会创建一个新的线程去执行任务,不管CPU有多忙,其实创建的线程太多是很消耗内存的,虽然主队列的target queue也有这个标识,但是主队列仅在主线程执行任务,因此主队列不会创建新的线程去执行任务) -
主队列以及全局队列都是系统底层中创建好的队列,它们只能获取得到,调用
dispatch_get_global_queue获取全局队列时根据队列优先级的不同,可以获取四种优先级不同的全局队列(它们都在底层根队列数组中,优先级由低到高获取索引分别为2、4、6、8),而通过dispatch_queue_create创建的队列会占用内存空间。 -
自定义串行队列以及主队列
width都为1,而自定义的并发队列以及全局队列width都大于1(前者为0xFFE,后者为0xFFF),width代表的是并发数,也就是说自定义串行队列以及主队列中的任务是一个接一个执行的,而全局队列以及并发队列中的任务可以多个并发执行。 -
每个队列的
serialnum都不一样,并且每个队列的serialnum都是唯一的,主队列为1,根队列数组中的队列serialnum是从4开始,并且依次递增,最后一个为15,系统中两个管理队列分别为2以及3,默认运行循环根队列的serialnum为16,自定义的队列serialnum从17开始,每创建一个队列,其serialnum都会加1。
3.2 同异步函数源码探究
不管是同步函数或者是异步函数,我们发现其第二个参数类型为dispatch_block_t,因此,我们先看看底层dispatch_block_t这个数据结构,代码如下所示:
//object.h 文件中
typedef void (^dispatch_block_t)(void);
//introspection_private.h 文件中
typedef struct Block_layout *dispatch_block_t;
//Block_private.h 文件中
struct Block_layout {
void *isa; //指向class对象
volatile int32_t flags; //状态标志位,包含引用计数
int32_t reserved; //预留内存大小
void (*invoke)(void *, ...); //指向块实现的函数指针
struct Block_descriptor_1 *descriptor;
// imported variables
};
struct Block_descriptor_1 {
unsigned long int reserved; //预留内存大小
unsigned long int size; //块大小
};
实际上这也是OC中block的底层实现结构,我们定义的Block实际上就是(struct Block_layout *)类型的结构体指针。
3.2.1 同步函数源码探究(上)
首先我们知道同步函数dispatch_sync会传入两个参数,参数1是dispatch_queue_t类型变量,而参数2是一个闭包,当参数1``dispatch_queue_t类型变量是主队列、串行队列或者并发队列时,函数执行情况是不一样的,但是我们又不知道源码流程,因此我们采用动态调试的方式依次来分析。
3.2.1.1 同步函数--串行队列(死锁的本质)
编写测试代码,如下所示:
- (void)deadlock {
dispatch_queue_t serialQueue = dispatch_queue_create("queue", NULL);
NSLog(@"1: CurrentThread: %@, Thread Mach_Port: %u", [NSThread currentThread], pthread_mach_thread_np(pthread_self()));
dispatch_sync(serialQueue, ^{
NSLog(@"2: CurrentThread: %@, Thread Mach_Port: %u", [NSThread currentThread], pthread_mach_thread_np(pthread_self()));
dispatch_sync(serialQueue, ^{
NSLog(@"3: CurrentThread: %@, Thread Mach_Port: %u", [NSThread currentThread], pthread_mach_thread_np(pthread_self()));
});
});
NSLog(@"4: CurrentThread: %@, Thread Mach_Port: %u", [NSThread currentThread], pthread_mach_thread_np(pthread_self()));
}
运行编译程序,程序崩溃,函数调用堆栈以及报错信息如下图所示:
而控制台打印输出信息如下图所示:
因此,在第一次调用dispatch_sync函数执行serialQueue中的任务时并不会发生错误,崩溃的原因就在于第二次调用dispatch_sync函数的过程中,所有我们先来探究第一次调用dispatch_sync时底层都做了些什么工作。
经过动态调试,在首次调用dispatch_sync函数时,底层函数调用顺序是这样的
dispatch_sync->_dispatch_sync_f->_dispatch_barrier_sync_f->_dispatch_lane_barrier_sync_invoke_and_complete->_dispatch_client_callout。
因此来查看dispatch_sync函数的源码,如下图所示:
红框中if语句块未执行的原因是因为我们传入的dispatch_block_t类型闭包一般都是无返回值、无参数的,而_dispatch_block_has_private_data函数会判断传入的参数work``中的函数指针是不是一个特殊的调用(有返回值、有参数),代码如下所示:
DISPATCH_ALWAYS_INLINE
static inline bool
_dispatch_block_has_private_data(const dispatch_block_t block)
{
return (_dispatch_Block_invoke(block) == _dispatch_block_special_invoke);
}
#define _dispatch_Block_invoke(bb) \
((dispatch_function_t)((struct Block_layout *)bb)->invoke)
void (*const _dispatch_block_special_invoke)(void*) = DISPATCH_BLOCK_SPECIAL_INVOKE;
因此才会调用_dispatch_sync_f函数,然后将work(struct Block_layout *类型)作为参数2,而_dispatch_Block_invoke宏的定义如下所示:
#define _dispatch_Block_invoke(bb) \
((dispatch_function_t)((struct Block_layout *)bb)->invoke)
typedef void (*dispatch_function_t)(void *_Nullable);
其实就是获取work中的成员变量invoke(函数指针)作为参数3调用_dispatch_sync_f函数,而实际上调用的是_dispatch_sync_f_inline函数,其部分代码如下图所示:
红框1中if语句会判断当前队列是不是串行队列(主队列也是串行的),如果是就调用_dispatch_barrier_sync_f函数,其实是调用的_dispatch_barrier_sync_f_inline函数,其部分代码如下所示:
红框1:_dispatch_tid_self宏及其相关宏定义如下所示:
#define _dispatch_tid_self() ((dispatch_tid)_dispatch_thread_port())
#define _dispatch_thread_port() pthread_mach_thread_np(_dispatch_thread_self())
#define _dispatch_thread_self() ((uintptr_t)pthread_self())
typedef mach_port_t dispatch_tid;
typedef uint32_t mach_port_t;
其中pthread_mach_thread_np与pthread_self函数都是pthread库中的函数,pthread_self是获取当前线程的id(实际上是线程控制块(tcb)的首地址),而pthread_mach_thread_np函数是将这个线程控制块的地址映射为mach端口号(每个线程都是唯一的,可以用来表示线程的ID)。
-
红框2:判断队列是不是(struct Dispatch_lane_s类型),如果不是,就会崩溃报错。 -
红框3:接着调用_dispatch_queue_try_acquire_barrier_sync函数,实际是调用了_dispatch_queue_try_acquire_barrier_sync_and_suspend函数,其代码如下图所示:
如果将_dispatch_queue_try_acquire_barrier_sync_and_suspend函数中的宏全部展开,所得代码如下所示:
DISPATCH_ALWAYS_INLINE DISPATCH_WARN_RESULT
static inline bool
_dispatch_queue_try_acquire_barrier_sync_and_suspend(dispatch_lane_t dq,
uint32_t tid, uint64_t suspend_count) {
//0x1FFE0000000000
uint64_t init = DISPATCH_QUEUE_STATE_INIT_VALUE(dq->dq_width);;
//DISPATCH_QUEUE_WIDTH_FULL_BIT: 队列宽度全部被使用的标记 0x0020000000000000ull 第53位
//DISPATCH_QUEUE_IN_BARRIER(第54位 0x0040000000000000ull) 队列已被关入栅栏 ,必须先执行完队列中的任务才能执行之后添加的任务。
//_dispatch_lock_value_from_tid: 获取当前线程id,存储在第2 ~ 31位,(0x103 & 0xfffffffc)
//DISPATCH_QUEUE_UNCONTENDED_SYNC:非竞争同步标记,存储在第1位
uint64_t value = DISPATCH_QUEUE_WIDTH_FULL_BIT | DISPATCH_QUEUE_IN_BARRIER | _dispatch_lock_value_from_tid(tid) | 0x00000002//0x60000000000102
uint64_t old_state, new_state;
bool _result = false;
//获取指向队列成员变量dq_state的指针_dq_state
uint64_t * _dq_state = &dq->dq_state;
//从_dq_state指针地址中读取数据,也就是dq_state的值
old_state = os_atomic_load(_dq_state, relaxed);
do {
//获取队列状态中的任务状态
uint64_t role = old_state & DISPATCH_QUEUE_ROLE_MASK;
//判断队列的状态是不是初始状态,如果不是就直接跳出循环,返回_result(false)
if (old_state != (init | role)) {
//实际上就是break;跳出循环
os_atomic_rmw_loop_give_up(break);
}
//新状态 =(此队列的各个标记位的新状态)+ 旧状态中的任务状态
new_state = value | role;
//临时存储old_state的值
uint64_t _r = old_state;
/*__c11_atomic_compare_exchange_weak函数的作用
当前值(参数1)与期望值(参数2)相等时,修改当前值为设定值(参数3),返回true
当前值(参数1)与期望值(参数2)不等时,将期望值修改为当前值(参数1),返回false
这个函数可能在满足true的情况下仍然返回false,所以只能在循环里使用,
*/
result = __c11_atomic_compare_exchange_weak(_dq_state, &old_state, new_state, memory_order_acquire, memory_order_relaxed);
old_state = _r;
} while (unlikely(!_result));
return _result;
}
分析以上代码,serialQueue串行队列创建时其状态初始值为(DISPATCH_QUEUE_STATE_INIT_VALUE(width),width为1),其初始状态与init的值是一样的,调用同步函数dispatch_sync执行队列中的任务,队列的状态值就会改变,value就是serialQueue队列改变后的状态值(栅栏标记被置为1,并且将当前线程ID信息存储到队列状态中),然后在do while循环中,首先获取serialQueue队列role值,判断oldState等不等于(init | role)队列刚创建时状态的初始值,如果不等于,就直接返回false,这里显然是等于,然后设置newState值为value | role,然后调用__c11_atomic_compare_exchange_weak函数,这里old_state的值与_dq_state指针明显是一样的,因此将serialQueue队列中的da_state值更新为newState,_result为true,然后跳出do while循环,_dispatch_queue_try_acquire_barrier_sync函数的返回值为true,所以在_dispatch_barrier_sync_f_inline函数中不会调用_dispatch_sync_f_slow函数,接着分析以下代码:
稍微分析一下,你就可以知道红框4中if语句的代码不会执行,然后会执行_dispatch_lane_barrier_sync_invoke_and_complete函数中的代码,如下图所示:
调用了红框1所示函数_dispatch_sync_function_invoke_inline中的代码,其代码如下图所示:
在这个函数中就看到了我们所熟悉的宏_dispatch_client_callout,ctxt是(struct Block_layout *)类型指针,func是闭包的函数地址指针,这个宏定义如下所示:
#define _dispatch_client_callout _dispatch_trace_client_callout
_dispatch_trace_client_callout实际上是一个内联函数,其定义如下图所示:
在这个函数中,重点就在于_dispatch_client_callout函数的调用,其代码如下所示:
红框中的代码一旦执行,那么dispath_sync函数中参数2``Block中的代码块就会调用,然后就会接着调用第二个dispatch_sync。
经过动态调试,在调用第二个dispatch_sync函数时,底层函数调用顺序是这样的
dispatch_sync->_dispatch_sync_f->_dispatch_barrier_sync_f->_dispatch_sync_f_slow->__DISPATCH_WAIT_FOR_QUEUE__,在调用__DISPATCH_WAIT_FOR_QUEUE__函数时崩溃报错,关键就在于是如何调用到_dispatch_sync_f_slow函数的,就是调用_dispatch_queue_try_acquire_barrier_sync函数后的返回值一定为false,返回false的原因是由于队列的da_state已经不是初始值了,_dispatch_sync_f_slow函数部分代码如下所示:
红框1处的代码是判断队列的targetq是否为空,如果为空就调用_dispatch_sync_function_invoke函数,主队列、自定义串行队列以及自定义并发队列的target queue都不为空,因此不会调用这个函数,红框2处的代码是将队列、任务闭包、闭包函数指针、当前线程优先级以及线程ID都存储到(struct dispatch_sync_context_s类型)变量dsc(同步调度上下文),红框4处调用的函数__DISPATCH_WAIT_FOR_QUEUE__部分代码如下所示:
_dispatch_wait_prepare函数获取队列状态,然后调用了_dq_state_drain_locked_by函数进行判断,其部分代码如下所示:
DISPATCH_ALWAYS_INLINE
static inline bool
_dq_state_drain_locked_by(uint64_t dq_state, dispatch_tid tid)
{
return _dispatch_lock_is_locked_by((dispatch_lock)dq_state, tid);
}
DISPATCH_ALWAYS_INLINE
static inline bool
_dispatch_lock_is_locked_by(dispatch_lock lock_value, dispatch_tid tid)
{
// equivalent to _dispatch_lock_owner(lock_value) == tid
return ((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0;
}
#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc)
其实就是获取到serialQueue队列状态中存储的线程ID值,由于之前首次调用dispatch_sync函数时已经存储了当前线程的ID,因此在第二次调用dispatch_sync函数时会判断得到所存储的线程ID与当前线程ID一致,就会返回true,然后程序就会崩溃报错,报错原因就是使用dispatch_sync函数的serialQueue队列已经被当前线程所持有了,也就是说当前线程正在同步执行serialQueue队列中的任务1了,任务1还没执行完,现在又想使用当前线程同步执行serialQueue队列的任务2,那么这就违背了串行队列的性质(FIF0,串行队列中的任务是顺序执行的),这就是产生死锁的根本性原因。
下篇文章: