iOS底层学习——GCD函数和队列原理探索
开发中,我们常用GCD
来处理一些异步流程,感觉很熟悉,但是又很陌生。一些概念还是很模糊,比如GCD
是什么,任务
是什么,串行队列
和并发队列
区别,同步
函数和异步
函数,队列和函数的配合使用,GCD
下层封装等等。本篇我们来一一分析。
1.GCD相关概念
1.GCD
GCD
全称是 Grand Central Dispatch
,纯C
语⾔,提供了⾮常多强⼤的函数。
GCD
的优势:
GCD
是苹果公司为多核的并⾏运算提出的解决⽅案GCD
会⾃动利⽤更多的CPU
内核(⽐如双核、四核)GCD
会⾃动管理线程的⽣命周期(创建线程、调度任务、销毁线程)- 程序员只需要告诉
GCD
想要执⾏什么任务,不需要编写任何线程管理代码
总结:GCD将任务添加到队列,并指定执行任务的函数。
2.任务
任务
封装成block
,任务的block
没有参数也没有返回值。任务通过队列的调度,由线程来执行。
任务是如何封装并调用的呢?这是一个问题!
3.函数
执行任务的函数分为:异步函数和同步函数
- 异步函数
dispatch_async
- 不⽤等待当前语句执⾏完毕,就可以执⾏下⼀条语句
- 会开启线程执⾏
block
的任务 - 异步是多线程的代名词
- 同步函数
dispatch_sync
- 必须等待当前语句执⾏完毕,才会执⾏下⼀条语句
- 不会开启线程
- 在当前线程执⾏
block
的任务
4.队列
队列分为两种:串行队列
和并发队列
。不同的队列中,任务排列的方式是不一样的,任务通过队列的调度,由线程池安排的线程来执行。
不管是串行队列
还是并发队列
,都会遵循FIFO
的原则,即先进入先调度的原则
;任务的执行速度或者说执行时长,与各自任务的复杂度有关。
串行队列
:通路比较窄,任务按照一定的顺序进行排列,一个一个执行并发队列
:通道比较广,同一时间可能有多个任务执行
队列是什么,如何封装的,如何调度任务的,这也是我们需要研究的内容。
5.队列与函数
上面理解了队列、函数、任务的区别,队列用来调用任务,函数用来执行任务。那么队列和函数不同的配合会有怎样的运行效果呢?
同步函数串行队列
- 不会开启线程,在当前线程中执行任务
- 任务串行执行,任务一个接着一个执行
- 会产生阻塞
同步函数并发队列
- 不会开启线程,在当前线程中执行任务
- 任务一个接着一个执行
异步函数串行队列
- 会开启一个线程
- 任务一个接着一个执行
异步函数并发队列
- 开启线程,在当前线程执行任务
- 任务异步执行,没有顺序,
CPU
调度有关
2.GCD相关案例分析
1.任务耗时分析
引入一个案例,以此来分析不同函数执行任务的耗时性。见下图:
-
两次计时之间,什么也不做,只创建了一个串行队列,耗时
0.000001
秒,见下图: -
在主线程中执行
textMethod
,耗时时间有所增加,0.000142
秒,见下图: -
将任务放入一个串行队列,并同步执行该任务,耗时
0.000117
秒。见下图: -
将任务放入串行队列,并异步执行该任务,异步函数会开启一个线程,并执行该任务。耗时
0.000007
秒,见下图:
通过上面的案例可以得出以下结论:
- 不管是采用什么方式,只要执行任务都会耗时
- 异步执行是耗时相对较少,异步可以用来解决我们在开发中的并发、多线程等问题
下面结合一些比较有代表性的案例进行分析
2.主队列添加同步任务
在当前的main队列
中添加一个任务,并同步执行该任务会怎么样呢?会崩
!见下图:
因为在当前的流程中,默认队列就是主队列
,也是一个串行队列
,任务执行的顺序是:
NSlog(@"0")
dispathc_sync任务块
NSlog(@"2")
而此时第二步中的块任务,会向当前的的主队列中添加一个任务NSlog(@"1")
。此时就产生相互等待
的问题,也就是我们常说的死锁
!为什么呢?见下图:
因为当前是一个串行队列,dispathc_sync任务块
要执行完成需要执行NSlog(@"1")
,而NSlog(@"1")
需要等到NSlog(@"2")
执行完成后才能执行,而NSlog(@"2")
又要等dispathc_sync任务块
执行完成才会执行。这样主队列就会进入一个相关等待
状态,也就是死锁
!验证一下,见下图:
果然报错,如何解决这个问题呢?将主队列
改成自定义的串行队列
。见下图:
3.并发队列添加异步任务
下面的案例,是一个并发队列,通道比较宽,所以这个嵌套过程并不会引起崩溃!见下图:
运行结果是怎样的?
- 因为任务复杂度基本一致,所以打印顺序:
1-5-2-4-3
; dispathc_async
会开启一个新的线程去执行其中的任务;
验证一下,见下图:
-
如果将异步函数改成同步函数会怎样的?
因为并发队列,所以不会导致队列任务的阻塞,同时因为是同步执行,所以不会开启新的线程,按照顺序去执行流程。见下图:
4.串行队列添加同步任务
参考下面的案例,创建一个串行队列,并向该串行队列中添加同步任务3
。运行结果会怎样的呢?
-
其实该案例和主队列添加同步任务是一样的,主队列也是串行队列。此案例也会崩溃!为什么?见下面串行队列的任务图:
从任务2开始进入一个串行队列,
dispathc_sync任务块
要执行完成需要执行NSlog(@"3")
,而NSlog(@"3")
需要等到NSlog(@"4")
执行完成后才能执行,而NSlog(@"4")
又要等dispathc_sync任务块
执行完成才会执行。这样串行队列就会进入一个相关等待
状态,也就是死锁
!虽然此时是在子线程中,但是队列通道遵循FIFO
原则,并且必须等待当前语句执⾏完毕,才会执⾏下⼀条语句。所以会崩溃!
验证一下:
5.并发队列多任务
并发队列中,添加同步任务和异步任务,会有怎样的结果呢?见下面案例:
结果分析:
- 主队列中有
10
个任务 - 其中
任务3
和任务0
是同步任务,所以任务3
一定在任务0
前面 任务1
、任务2
、任务3
的顺序是不确定的任务7
、任务8
、任务9
一定在任务0
的后面
3.队列源码探索
要想研究其内部实现,找到源码的出处,通过汇编查看,没有找到相关出处。我们可以通过下dispatch_queue_create
符号断点,查看diaspatch
的出处。见下图:
GCD
来自libdispatch.dylib动态库
。下载libdispatch.dylib源码
,探索GCD
!
1.主队列
-
dispatch_get_main_queue()
分析进入主队列定义的地方,见下图:
从上面的注释中我们可以发现一些内容:
- 主队列在应用程序中,用于主程序和
main Runloop
- 主队列的创建在
main函数
之前即完成,也即是dyld应用程序
加载阶段即已创建
主队列是通过
DISPATCH_GLOBAL_OBJECT
获取。全局搜索DISPATCH_GLOBAL_OBJECT
,该函数是通过宏进行定义的,定义如下:#define DISPATCH_GLOBAL_OBJECT(type, object) ((type)&(object))
其中第一个参数作为一个队列类型,第二个参数为主队列对象,可以理解为通过类型和主队列对象,封装后得到
main
队列。全局搜索_dispatch_main_q =
,找到了主队列初始化定义的地方,见下图:主队列是一个结构体,并且继承自
DISPATCH_GLOBAL_OBJECT_HEADER
,全局搜索DISPATCH_GLOBAL_OBJECT_HEADER
,其定义如下图: - 主队列在应用程序中,用于主程序和
-
通过
lable
查找主队列定义上面的分析方式其实有些取巧,下面通过
label
来定位主队列。在主线程中进行debug
调试时,运行堆栈信息中,能看到下图的内容:图中可以看到线程以及线程执行的任务栈信息,在主线程中,可以看到队列是一个
serial
类型,并且其对应的lable
是com.apple.main-thread
。通过上面线索,分别打印
自定义串行队列
、并发队列
、主队列
、全局队列
。见下图:在创建自定义队列时,需要传入队列的名称,也就是队列
lable
,以及队列的类型。通过上面的打印信息,队列都有其对应的名称。根据队列的label
,在libdispatch.dylib源码
中进行全局搜索。见下图:最终定位到的位置和
dispatch_get_main_queue()
分析时结果是一样的,主队列是一个结构体
,并且com.apple.main-thread
是默认label
。 -
那么主队列是在何处初始化的呢?
前面查看主队列定义的注释时,已经有说明,在
main函数
之前进行了初始化,我们在研究dyld
时,分析objc_init()
初始化时得出过这样的结论:libSystem_init -> libdispatch_init -> objc_init
。那么主队列的初始化是否在dispatch_init()
方法中呢?在
dispatch_init()
中成功找到了主队列初始化的地方,获取默认队列,并将主队列地址绑定到当前队列和主线程中
。 -
总结
主队列在
main
函数之前,应用程序加载调用dispatch_init()
方法时,即完成创建。主队列为当前的默认队列,并绑定到主线程中。
还有个问题没有搞清楚,通过debug调试可以看到主队列是串行队列serial,但是串行队列在哪里确定下来的呢?在下面分析!
2.全局队列
-
dispatch_get_global_queue(0, 0)
进入全局队列定义的地方,见下图:
创建全局并发队列时可以传参数,根据不同服务质量或者优先等级提供不同的并发队列。那么我们可以得出一个结论:
应该有一个全局的集合,去维护这些并发队列。
-
用全局队列的
lable
-com.apple.root.default
搜索得到一个队列集合,根据不同的服务质量提供不同的全局队列,见下图:
-
总结
系统会维护一个全局队列集合,根据不同的服务质量或者优先等级提供不同的全局队列。我们在开发工作中默认使用:
dispatch_get_global_queue(0, 0)
。
3.自定义队列创建过程
由于libdispach.dylib
的源码不能编译,只能通过关键步骤逐步去定位分析。在源码中全局搜索dispatch_queue_create
,结果很多,因为创建时第一个参数是一个常量,优化搜索条件,全局搜索dispatch_queue_create(const
,找到创建队列的入口,见下图:
-
lable
为队列名称 -
attr
当前传入的,要创建的队列类型 -
接续调用
_dispatch_lane_create_with_target
方法,并传入默认队列类型通过上图可以明确,默认队列被定义为
NULL
。
接着全局搜索_dispatch_lane_create_with_target(const
,定位到队列创建的地方,见下面源码:
// lable -> 名称
// dqa -> 要创建的类型
// tq -> 默认类型 - 串行NULL
DISPATCH_NOINLINE
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
dispatch_queue_t tq, bool legacy)
{
// dqai 创建 - dqa传入的属性串行还是并行
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
//
// Step 1: Normalize arguments (qos, overcommit, tq)
//
dispatch_qos_t qos = dqai.dqai_qos;
#if !HAVE_PTHREAD_WORKQUEUE_QOS
if (qos == DISPATCH_QOS_USER_INTERACTIVE) {
dqai.dqai_qos = qos = DISPATCH_QOS_USER_INITIATED;
}
if (qos == DISPATCH_QOS_MAINTENANCE) {
dqai.dqai_qos = qos = DISPATCH_QOS_BACKGROUND;
}
#endif // !HAVE_PTHREAD_WORKQUEUE_QOS
_dispatch_queue_attr_overcommit_t overcommit = dqai.dqai_overcommit;
if (overcommit != _dispatch_queue_attr_overcommit_unspecified && tq) {
if (tq->do_targetq) {
DISPATCH_CLIENT_CRASH(tq, "Cannot specify both overcommit and "
"a non-global target queue");
}
}
if (tq && dx_type(tq) == DISPATCH_QUEUE_GLOBAL_ROOT_TYPE) {
// Handle discrepancies between attr and target queue, attributes win
if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
if (tq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) {
overcommit = _dispatch_queue_attr_overcommit_enabled;
} else {
overcommit = _dispatch_queue_attr_overcommit_disabled;
}
}
if (qos == DISPATCH_QOS_UNSPECIFIED) {
qos = _dispatch_priority_qos(tq->dq_priority);
}
tq = NULL;
} else if (tq && !tq->do_targetq) {
// target is a pthread or runloop root queue, setting QoS or overcommit
// is disallowed
if (overcommit != _dispatch_queue_attr_overcommit_unspecified) {
DISPATCH_CLIENT_CRASH(tq, "Cannot specify an overcommit attribute "
"and use this kind of target queue");
}
} else {
if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
// Serial queues default to overcommit!
overcommit = dqai.dqai_concurrent ?
_dispatch_queue_attr_overcommit_disabled :
_dispatch_queue_attr_overcommit_enabled;
}
}
if (!tq) {
tq = _dispatch_get_root_queue(
qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos,
overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq;
if (unlikely(!tq)) {
DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
}
}
//
// Step 2: Initialize the queue
//
if (legacy) {
// if any of these attributes is specified, use non legacy classes
if (dqai.dqai_inactive || dqai.dqai_autorelease_frequency) {
legacy = false;
}
}
// 类的类型
const void *vtable; // - 设置类 类是通过宏定义拼接而成
dispatch_queue_flags_t dqf = legacy ? DQF_MUTABLE : 0;
if (dqai.dqai_concurrent) {
// OS_dispatch_##name##_class
// OS_dispatch_queue_concurrent - 宏定义拼接类类型
vtable = DISPATCH_VTABLE(queue_concurrent);
} else {
vtable = DISPATCH_VTABLE(queue_serial);
}
switch (dqai.dqai_autorelease_frequency) {
case DISPATCH_AUTORELEASE_FREQUENCY_NEVER:
dqf |= DQF_AUTORELEASE_NEVER;
break;
case DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM:
dqf |= DQF_AUTORELEASE_ALWAYS;
break;
}
if (label) {
const char *tmp = _dispatch_strdup_if_mutable(label);
if (tmp != label) {
dqf |= DQF_LABEL_NEEDS_FREE;
label = tmp;
}
}
// dq创建的地方 - alloc init 队列也是个对象
// OS_dispatch_queue_serial
// OS_dispatch_queue_concurrent
// 即然是对象应该有ISA的走向
dispatch_lane_t dq = _dispatch_object_alloc(vtable, // 类
sizeof(struct dispatch_lane_s)); // alloc
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ? // 区分串行和并发队列
DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // init
dq->dq_label = label;
dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
dqai.dqai_relpri);
if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
}
if (!dqai.dqai_inactive) {
_dispatch_queue_priority_inherit_from_target(dq, tq);
_dispatch_lane_inherit_wlh_from_target(dq, tq);
}
_dispatch_retain(tq);
dq->do_targetq = tq;
_dispatch_object_debug(dq, "%s", __func__);
return _dispatch_trace_queue_create(dq)._dq; // 通过这层可以理解为经过一层封装后,返回的依然是dq
}
下面深入研究该方法的核心功能点。该方法的返回值为dq
,虽然调用了_dispatch_trace_queue_create
方法对dq
进行了封装处理,但是最终返回的依然是dq
,所以dq
才是我们的研究重点!
-
dq
创建流程// dq创建方 - alloc init 队列也是个对象 // OS_dispatch_queue_serial // OS_dispatch_queue_concurrent // 即然是对象应该有ISA的走向 dispatch_lane_t dq = _dispatch_object_alloc(vtable, // 类 sizeof(struct dispatch_lane_s)); // alloc _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ? // 区分串行和并发队列 DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER | (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // init dq->dq_label = label; dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos, dqai.dqai_relpri); if (overcommit == _dispatch_queue_attr_overcommit_enabled) { dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT; } if (!dqai.dqai_inactive) { _dispatch_queue_priority_inherit_from_target(dq, tq); _dispatch_lane_inherit_wlh_from_target(dq, tq); } _dispatch_retain(tq); dq->do_targetq = tq; _dispatch_object_debug(dq, "%s", __func__);
-
调用
_dispatch_object_alloc
方法进行初始化,内存申请调用
_dispatch_object_alloc
方法进行队列对象的初始化。与NSObject对象
初始化类似,传入创建对象的模板和大小。见下图:然后调用
_os_object_alloc_realized
方法,完成初始化流程。在该函数中,通过调用calloc
进行初始化,同时isa
指向cls
,也就是vtable
。见下图: -
_dispatch_queue_init
为构造函数在调用这个方法时,第三个参数是:
dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1
不难理解,判断是否为并发队列,如果是,传入
DISPATCH_QUEUE_WIDTH_MAX
,否则传入1
。也就是说,串行队列这里传入1
,如果是并发队列,则传入DISPATCH_QUEUE_WIDTH_MAX
。其定义见下图:进入
_dispatch_queue_init
构造函数的实现,发现重要的线索,DQF_WIDTH(width);
也就是用来确定队列的类型,以此来区分串行队列和并发队列,见下图:到这里我们也就可以确定主队列的类型,在主队列的结构体定义中,DQF_WIDTH(1);,所以主队列是串行队列。
-
对
dq
进行设置,如dq_label
、dq_priority
等
-
dq
的创建初始化流程,已经清楚了,那在dp
初始化过程中传入的参数vtable
、dqai
、dqf
,分别是什么,又起到什么作用呢?我们继续分析。
-
dqai
初始化// dqai 创建 - dqa传入的属性串行还是并行 dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
在
_dispatch_lane_create_with_target
第一行,就进行了dqai
的初始化,其中参数dqa
为要创建的队列的类型。查看_dispatch_queue_attr_to_info
源码:初始化了
dqai
,并判断dqa
的类型,如果是并发队列,则设置并发队列为true
,否则默认为串行队列。在调用_dispatch_queue_init
对dq
进行构造时,对队列类型进行了区分,也就是DQF_WIDTH(width);
的传参,串行队列width=1
,否则为并发队列。 -
vtable
vtable
是什么呢?可以理解为是一个类,或者说构造队列的模板类
。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); }
dqai
在前面创建时会区分队列的类型
,根据队列的类型来初始化不同的vtable
。DISPATCH_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
对应的就是队列的类型。通过拼接完成类的定义,这和我们在应用层使用的队列类型是一致的,见下图: -
-
总结
至此我们可以总结一下自定义队列的创建过程。在底层,会根据上层传入的
队列名称lable
和队列类型
进程封装处理。根据类型初始化对应的vtable
,也就是对应的队列类(模板类)
。通过alloc
和init
方法完成队列的内存开辟和构造初始化过程,设置队列的对象的isa
指向,并完成队列类型的区分。
4.函数执行
上面我们已经总结了,执行任务的函数分为:异步函数和同步函数
- 同步函数
dispatch_sync
- 异步函数
dispatch_async
那么这些行数的执行逻辑是怎么样的呢?如何调度队列中的任务并处理任务?对于异步函数底层是如何开辟线程的呢?
1.同步函数
在libdispatch.dylib
源码中,全局搜索dispatch_sync(dis
,找到了同步函数的入口。见下图:
两个参数分别是:队列dq
和任务work
。继续跟踪,调用_dispatch_sync_f
函数,全局搜索_dispatch_sync_f(dis
,见下图:
继续跟踪源码,再全局搜索_dispatch_sync_f_inline(dis
,见下图:
至此:
- 封装流程
dispatch_sync
_dispatch_sync_f
_dispatch_sync_f_inline
- 参数
dq
:队列dq
ctxt
:任务work
func
: 宏定义的函数_dispatch_Block_invoke
#define _dispatch_Block_invoke(bb) \ ((dispatch_function_t)((struct Block_layout *)bb)->invoke)
那么在_dispatch_sync_f_inline
方法中,哪一行才是我们需要研究的重点呢?先简单分析一下,首先最前面的一段判断流程:
if (likely(dq->dq_width == 1)) {
return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags);
}
此部分显然是串行队列流程,因为上面已经分析了,dq_width=1
时,表示串行队列。那么_dispatch_sync_f_slow
和_dispatch_sync_recurse
会总哪一步呢?我们可以通过下符号断点来确定,通过下符号断点,发现自定义并发队列,其成功走到_dispatch_sync_f_slow
中。见下图:
进入_dispatch_sync_f_slow
方法中,见下图:
哪一步才是需要研究的呢?显然传入的地方不太可能是null
,所以大概率会走_dispatch_sync_function_invoke
方法,同样采用下符号断点的方式跟踪。见下图:
进入_dispatch_sync_function_invoke
方法后,会调用_dispatch_sync_function_invoke_inline
方法,见下图:
继续跟踪源码,进入_dispatch_sync_function_invoke_inline
实现中,见下图:
方法很多哪个方法才是我们所需要研究的重点呢?我们通过bt
看一下同步函数的运行堆栈,见下图:
_dispatch_client_callout
方法最终完成了任务的调用执行,进入_dispatch_client_callout
方法,见下图:
搜索发现_dispatch_client_callout
方法有很多,但是最终都通过func
完成了work
的调用执行。
-
总结
在同步流程中通过宏定义的函数
_dispatch_Block_invoke
,也就是func
,完成任务work
的执行。
2.异步函数
在libdispatch.dylib
源码中,全局搜索dispatch_async(dis
,找到了异步函数的入口。见下图:
-
任务的封装
_dispatch_continuation_init
在该方法里,对
work
进行了封装处理,其中方法_dispatch_continuation_init
可以理解为一个任务包装器,进入_dispatch_continuation_init
方法中查看实现:其中
_dispatch_Block_invoke
这个函数很熟悉,也就是同步函数中的func
。但是这里有一个判断,见下面代码:dispatch_function_t func = _dispatch_Block_invoke(work); if (dc_flags & DC_FLAG_CONSUME) { func = _dispatch_call_block_and_release; }
dc_flags
的初始值就是DC_FLAG_CONSUME
,所以&
处理后,这里会进入条件语句中,并且func
被重新设置为_dispatch_call_block_and_release
。进入
_dispatch_continuation_init_f
,见下图:在这里对将
work
和func
封装到dc
中,同时对任务的优先级进行处理,因为异步函数由于线程或者cpu
的调用是异步的,所以这里会对任务的调用优先级进行处理。
回到dispatch_async
方法实现,完成任务包装后,调用_dispatch_continuation_async
方法进行异步函数的处理流程。见下图:
很显然最后一行使我们索要研究的重点,dx_push
函数三个参数分别为:队列、dc
(包装的任务)、qos
。全局搜索dx_push
,找到了宏定义的地方,见下图:
看到vtable
感觉很熟悉,没错就是上面在分析队列的创建初始化时的模板类,也就是队列对应的类。这里会调用vtable
这个类的dq_push
方法,直接搜索dq_push
,找到其实现:
底层为不同类型的队列提供不同的调用入口,比如全局并发队列会调用_dispatch_root_queue_push
方法。依次作为入口,全局搜索_dispatch_root_queue_push
方法的实现:
在此流程中,前面只是做一些判断封装处理,最终会走到最后一行代码_dispatch_root_queue_push_inline
中,继续跟踪器源码流程:
在_dispatch_root_queue_push_inline
中调用了_dispatch_root_queue_poke
方法,_dispatch_root_queue_poke
中的核心流程为_dispatch_root_queue_poke_slow
,见下图所示:
-
_dispatch_root_queue_poke_slow
实现_dispatch_root_queue_poke_slow
中有一个关键流程,_dispatch_root_queues_init()
,见下图:进入
_dispatch_root_queues_init()
方法,在该方法中采用的单例处理,见下图: -
单例处理
_dispatch_root_queues_init_once
进入
_dispatch_root_queues_init_once
方法,这里做了什么呢?见下图:在该方法中进行了线程池的初始化处理、工作队列的配置、工作队列的初始化等工作,这也就是解释了为什么
_dispatch_root_queues_init_once
是单例的。单例可以避免重复的初始化。同时这里有一个关键的设置,执行函数的设置,也就是将任务执行的函数被统一设置成了
_dispatch_worker_thread2
,见下面代码:cfg.workq_cb = _dispatch_worker_thread2;
我们可以通过
bt
打印运行堆栈信息,来验证异步函数最终任务是通过_dispatch_worker_thread2
调用的。见下图所示: -
总结
通过跟踪异步处理流程,系统针对不同的队列类型,执行不同的
dq_push
的方法,并通过单例的形式完成了线程池的初始化、工作队列的配置等工作,并且底层最终通过_dispatch_worker_thread2
完成了异步函数中任务的调用执行。
5.遗留问题
GCD
的运用、底层原理、函数执行逻辑等,就分析到这里,但是还有一些问题没有解决,比如一下问题:
- 对于异步函数,线程在哪里开辟?
- 底层通过
_dispatch_worker_thread2
方法完成任务的执行,那么触发调用的位置在哪? GCD
中单例的逻辑是怎样的?
_dispatch_root_queue_poke_slow
方法中还有一些没有探索的东西!
下篇文章再深入分析!
转载自:https://juejin.cn/post/6991106328928190494