这是我参与8月更文挑战的第12天,活动详情查看: 8月更文挑战
GCD简介
什么是GCD
?
GCD
全称是Grand Central Dispatch
,它使用纯C
语言实现,提供了非常多强大的函数;
GCD
的优势:
GCD
是苹果公司为多核的并行运算提出的解决方案;GCD
会自动利用更多的CPU
内核,比如(双核,四核);GCD
会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
我们只需要告诉GCD
想要执行什么任务,不需要编写任何线程管理代码;
关于GCD
,有一句话需要我们记住:
GCD
将任务添加到队列,并且指定执行任务的函数;
那么这句话如何理解呢?我们通过一个例子来说明,请看如下代码:
- 1.我们有一个任务:
NSLog(@"Hello GCD")
,它被定义成为dispatch_block_t
形式的代码块(或者说任务块); - 2.指定(创建)一个
dispatch_queue_t
类型的队列,队列有两种:并发队列和串行队列 - 3.通过
异步函数
:dispatch_async
将任何添加到队列中(将任何和队列绑在一起);
函数
- 任务使用
block
封装; - 任何的
block
没有参数也没有返回值; - 执行任何的函数;
- 异步
dispatch_async
:- 不用等待当前语句执行完毕,就可以执行下一条语句;
- 会开启线程执行
block
的任务; - 异步是多线程的代名词;
- 同步
dispatch_sync
:- 必须等待当前语句执行完毕,才会执行下一条语句;
- 不会开启线程;
- 在当前执行
block
的任务
队列
队列分为:串行队列
和并发队列
;(队列也是一种数据结构),队列只有调度没有执行,执行需要线程来操作,而线程依赖于线程池;
串行队列
和并发队列
区别如下图所示:
串行队列
遵循FIFO
(First In First Out)原则;并发队列
先调度的并不一定先执行;并发队列只考虑调度顺序;在某一时刻,可能有多个任务都在调度;
函数与队列
- 同步函数串行队列
- 不会开启线程,在当前线程执行任务
- 任务串行执行,任务一个接着一个
- 会产生堵塞
- 同步函数并发队列
- 不会开启线程,在当前线程执行任务
- 任务一个接着一个
- 异步函数串行队列
- 开启线程,一条新线程
- 任务一个接着一个
- 异步函数兵法队列
- 开启线程,在当前线程执行任务
- 任务异步执行,没有顺序,与CPU调度有关
关于队列的一道面试题:
面试题
看如下代码的打印结果:
打印结果:
解析:
DISPATCH_QUEUE_CONCURRENT
说明queue
是一个并发队列
,而队列中任务的执行是一个耗时操作;所以1
和5
先打印;- 接下来执行
dispatch_async
中的任务,先打印2
; dispatch_sync
是一个同步函数
,所以dispatch_sync
会阻塞任务,打印3
;- 最后打印
4
;
所以最终打印: 1 5 2 3 4
接下来,我们对代码稍作修改:
最终执行代码会崩溃;
解析
1
和5
依然先打印;NULL
说明queue
是一个串行队列(DISPATCH_QUEUE_SERIAL)
,或者说是同步队列
;遵循FIFO
的原则,所以dispatch_async
中的代码执行顺序为:
解析:
由于是串行队列
,所以NSLog(@"2")
,dispatch_sync代码块
和NSLog(@"4")
三个任务会相继加入到队列中,NSLog(@"4")
要等待dispatch_sync代码块
执行完毕才能执行,而dispatch_sync代码块
又向队列中添加了NSLog(@"3")
的任务,任务NSLog(@"3")
在任务NSLog(@"4")
之后,所以要等待NSLog(@"4")
执行完毕才能执行,而NSLog(@"4")
又在等待dispatch_sync代码块
最终导致了死锁
;
dispatch_sync
阻塞了红色区域的整个代码块
;
所以最终打印结果:1 5 2 崩溃
死锁
的堆栈信息为_dispatch_sync_f_slow
;
那么引起死锁
的原因是什么呢?
- 主线程因为同步函数的原因等着先执行任务;
- 主队列等着主线程的任务执行完毕再执行自己的任务
- 主队列和主线程相互等待会造成死锁;
主队列与全局队列
- 主队列
- 专门用来在主线程上调度任务的串行队列
- 不会开启线程
- 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度
dispatch_get_main_queue()
- 全局并发队列
- 为了方便程序员的使用,苹果提供了全局队列
dispatch_get_global_queue(0,0)
- 全局队列是一个并发队列
- 在使用多线程开发时,如果队列没有特殊需求,在执行异步任务时,可以直接使用全局队列
- 为了方便程序员的使用,苹果提供了全局队列
主队列分析
我们常用的队列以下四种形式:
那么主队列dispatch_get_main_queue
是一个什么队列呢?我们点击进入发现,在dispatch_get_main_queue
的注释中有这样一行文字:
......
Because the main queue doesn't behave entirely like a regular serial queue,
......
the main thread before main() is called.
复制代码
意思是:主队列表现的并不像一个普通的串行队列;而且它在main
函数之前
那么,我们可以在main
函数上打上断点来验证一下:
主队列也是串行队列(serial)的一种;
那么,它是在什么时候创建,又是在怎么获取的呢?我们可以结合其源码来分析,那么它的源码在什么地方呢?我们执行一个测试函数:
可以发现,测试代码中block
的_block_invoke
来自于libdispatch.dylib
中的_dispatch_call_block_and_release
;
我们可以从源码地址获取libdispatch
的源码;
我们在源码中搜索dispatch_get_main_queue
:
dispatch_get_main_queue
调用了DISPATCH_GLOBAL_OBJECT
,并传递了两个参数dispatch_queue_main_t
和_dispatch_main_q
其宏定义如下:
#define DISPATCH_GLOBAL_OBJECT(type, object) ((type)&(object))
复制代码
也就是说dispatch_queue_main_t
是类型,_dispatch_main_q
才是它真正的对象;那么我们搜索_dispatch_main_q =
来寻找_dispatch_main_q
的赋值:
除了这种方式,我们也可以通过另一种方式来寻找切入点:
我们发现,创建队列时,我们自定义的label
能够打印出来,那么dispatch_get_main_queue
的label
字符串com.apple.main-thread
是否在源码中也有体现呢?
搜索此label
,同样可以定位到相同的位置;
那么我们怎么根据源码来论证dispatch_get_main_queue
是一个串行队列
呢?
串行队列
与并发队列
又有哪些不一样的特性呢?
由于串行队列
与并发队列
都是使用dispatch_queue_create
创建的,那么我们就以此为切入点,结合源码去分析底层逻辑;
串行和并发的底层源码分析
在libdispatch
源码中搜索dispatch_queue_create(const
;
为什么搜索呢,是因为在工程中进入
的声明,发现其声明为:
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label,
dispatch_queue_attr_t _Nullable attr);
复制代码
搜索dispatch_queue_create(const
可以定位到dispatch_queue_create
的实现:
调用了_dispatch_lane_create_with_target
,并且传递了label
,attr
,DISPATCH_TARGET_QUEUE_DEFAULT
等参数;我们进入_dispatch_lane_create_with_target
方法:
由于函数实现代码较多,只截取一部分;按照分析源码的思维逻辑,这个时候,我们需要研究其返回值,也就是return
的地方:
return _dispatch_trace_queue_create(dq)._dq;
复制代码
_dispatch_trace_queue_create
是一个追踪
函数,不是重点,所以,我们重点落在了参数dq
上,它是如何创建的呢:
_dispatch_object_alloc
:申请和开辟内存_dispatch_queue_init
:构造函数 初始化
我们在这里看到了一个重点:
dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1
复制代码
判断dqai
是否是并发
,如果是并发
,参数传递DISPATCH_QUEUE_WIDTH_MAX
,否则参数传1
;我们看一下_dispatch_queue_init
的实现:
它的第三个参数width
就是区分是并发队列
还是串行队列
的标识;
也就是如果DQF_WIDTH(width)
的width
是1
,那么就是串行队列,否则就是并发队列
,这跟我们上文分析dispatch_get_main_queue
时,最终定位的_dispatch_main_q
的赋值:
此处DQF_WIDTH(1)
传入了参数1
,再次说明验证了我们主队列是串行队列
的结论;
那么dq_serialnum
是干什么的呢?
我们看一下os_atomic_inc_orig
是什么:
#define os_atomic_inc_orig(p, m) \
os_atomic_add_orig((p), 1, m)
复制代码
而&_dispatch_queue_serial_numbers
在源码中搜索,可以定位到:
而宏定义DISPATCH_QUEUE_SERIAL_NUMBER_INIT
为17
:
根据注释,我们可以看出,这个宏定义标识了不同的队列,主队列时其值为1
;
那么DISPATCH_QUEUE_SERIAL_NUMBER_INIT
替换为17
,参数m
替换成relaxed
之后,可以得到:
#define os_atomic_inc_orig(p, m) \
os_atomic_add_orig((p), 1, relaxed)
复制代码
我们继续查看os_atomic_add_orig
:
#define os_atomic_add_orig(p, v, m) \
_os_atomic_c11_op_orig((p), (v), m, add, +)
复制代码
也就是:
#define os_atomic_add_orig(17, 1, relaxed) \
_os_atomic_c11_op_orig((17), (1), relaxed, add, +)
复制代码
继续看_os_atomic_c11_op_orig
:
#define _os_atomic_c11_op_orig(p, v, m, o, op) \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), v, \
memory_order_##m)
复制代码
也就是:
#define _os_atomic_c11_op_orig((17)), (1), relaxed, add, +) \
atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), v, \
memory_order_##m)
复制代码
##o##
:是一个连接符号,在编译的过程中会被删除,然后把参数o
传进来,拼接上;
最终宏定义变成:
#define _os_atomic_c11_op_orig((17)), (1), relaxed, add, +) \
atomic_fetch_add_explicit(_os_atomic_c11_atomic(p), v, \
memory_order_##m)
复制代码
atomic_fetch_add_explicit
是C++ 11
的原子操作函数:
atomic_fetch_add_explicit(volatile A * obj, M arg, memory_order order);
obj
:指向要修改的原子对象的指针;arg
:要添加到存储在原子对象中的值的值order
:此操作的内存同步排序,所有值都是允许的
其结果也就是把17
加上1
;
通过dqai.dqai_concurrent
的值可以知道队列是串行队列
还是并发队列
,那么dqai
是什么呢?
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
复制代码
dqai
就是我们所说的优先级
;_dispatch_queue_attr_to_info
将dqa
进行面向对象封装;
GCD底层源码继承链分析
在前文中我们创建了四个队列,但是不管是串行队列
还是并行队列
,最终都是使用dispatch_queue_t
这个类型来接收的;那么我们就以此为切入点,来分析一下dispatch_queue_t
是如何处理不同的队列的,点击dispatch_queue_t
跳转:
宏DISPATCH_DECL
的定义为:
其实有多处宏定义,但是最终的结果应该是一致的,所以我们找一个易于分析的来探索;
#define DISPATCH_DECL(name) \
typedef struct name##_s : public dispatch_object_s {} *name##_t
复制代码
此处name
为dispatch_queue
,那么宏定义可以转换为:
#define DISPATCH_DECL(dispatch_queue) \
typedef struct dispatch_queue_s : public dispatch_object_s {} *dispatch_queue_t
复制代码
那么typedef struct dispatch_queue_s : public dispatch_object_s {} *dispatch_queue_t
这串代码如何解释呢?
dispatch_queue_t
是一个类型,这个类型来自于dispatch_queue_s
类型的结构体;而结构体有继承自dispatch_object_s
;
dispatch_queue_t
->dispatch_queue_s
->dispatch_object_s
类似于
class
->objc_class
->objc_object
的继承关系;
GCD面试题
面试题一
@property (atomic, assign) int num;
while (self.num < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.num++;
});
}
NSLog(@"end : %d",self.num);
复制代码
解析:
- 当
sum
小于5
时,将一直会进入while
的死循环中,只要num
小于5
,就不会向下执行; - 异步函数
dispatch_async
会开辟新的线程去执行num++
操作,再循环执行的过程中,这些线程可能返回了,也可能没有返回,后台可能存在N多线程; - 当某一个线程的
num++
执行完之后,num
大于等于5
之后才会进行打印;
打印一个 大于等于5的数字
面试题二
@property (atomic, assign) int num;
for (int i= 0; i<10000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.num++;
});
}
NSLog(@"end : %d",self.num);
复制代码
解析:
- for循环,直接循环10000次
- 异步函数
dispatch_async
会开辟新的线程执行num++
操作,当10000
次循环执行完毕的时候,这些线程中肯定有线程未返回;
打印一个 小于等于10000的数字
GCD的任务执行流程
同步流程
我们来看一个同步
的GCD函数
:
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"GCD 函数分析");
});
复制代码
我们都知道这个方法能够打印出GCD 函数分析
,但是这个block
代码块是什么时候被执行的呢?
我们看一下dispatch_sync
函数的声明:
然后在源码中搜索dispatch_sync(dispatch_queue_t
可以找到其实现:
我们研究的是代码块
的执行,也就是work
,所以我们只要留意work
的调用就可以了,但是此处的_dispatch_sync_block_with_privdata
和_dispatch_sync_f
究竟走的哪个函数呢?我们在GCD
函数调用的时候添加断点,然后添加两个函数的符号断点
,继续运行:
发现走的是函数_dispatch_sync_f
,搜索_dispatch_sync_f(dispatch_queue_t
:
调用了_dispatch_sync_f_inline
,参数ctxt
就是work
,而func
也与work
有关,我们继续向下搜索_dispatch_sync_f_inline(dispatch_queue_t
:
此处分支太多,我们在工程中继续添加_dispatch_barrier_sync_f
,_dispatch_sync_f_slow
,_dispatch_sync_recurse
,_dispatch_sync_invoke_and_complete
四个符号断点,向下执行:
执行了_dispatch_sync_f_slow
,点击进入这个方法的实现:
此方法中和ctxt
与func
有关的方法有两个_dispatch_sync_function_invoke
和_dispatch_sync_invoke_and_complete_recurse
,分别下符号断点,继续运行:
点击进入_dispatch_sync_function_invoke
:
点击进入_dispatch_sync_function_invoke_inline
:
此处跟ctxt
和func
有关的只有_dispatch_client_callout
,点击进入方法:
最终在这个地方执行了回调;那么究竟是不是这样呢?我们在工程中打印一下堆栈
验证一下:
验证结果与我们分析的结果一致;
异步流程
我们来看一个同步
的GCD函数
:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"GCD 函数分析");
});
复制代码
其函数声明如下:
我们在源码中搜索它的实现:
这里边和work
有关的方法是_dispatch_continuation_init
,点击进入其实现:
这里边与ctxt
和work
有关的函数是_dispatch_continuation_init_f
,点击进入这个方法:
在这里ctxt
和f
分别赋值给了dc
的dc_ctxt
和dc_func
两个成员变量,进入_dispatch_continuation_priority_set
函数:
这里是针对qos
的封装(优先级),那就是说在_dispatch_continuation_init
这个函数的分支里边是针对任何和优先级的封装,那么为什么Apple
要在这个地方针对异步函数
做此操作呢?
因为异步函数异步调用,里边会出现无序的情况,那么就需要优先级作为参考和衡量的依据,然后根据
CPU
的调度情况进行调度;
那么封装qos
之后,如何操作呢?我们点击进入_dispatch_continuation_async
方法:
_dispatch_trace_item_push
是个追踪函数,可以忽略;那么我们着手分析dx_push
这个宏定义:
#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
复制代码
因为参数dos
对应着z
,所以此处我们应该分析宏定义dp_push
,dx_vtable(x)->dq_push(x, y, z)
此处是一个函数的调用,那么一定存在这对应函数的赋值操作:
针对队列的不同,赋值不同的值,全局并发队列
赋值情况为_dispatch_root_queue_push
,搜索定位:
此处与qos
有关的是_dispatch_root_queue_push_override
,但由于宏定义HAVE_PTHREAD_WORKQUEUE_QOS
为0
,所以此函数并不执行,将会执行(void)qos
,然后调用_dispatch_root_queue_push_inline
函数:
继续进入_dispatch_root_queue_poke
函数:
然后进入_dispatch_root_queue_poke_slow
:
此方法过于庞大,我们无从入手,但是我们发现有一个初始化方法_dispatch_root_queues_init
,我们试着看看这个方法的实现:
dispatch_once_f
是一个单例
的调用,调用了_dispatch_root_queues_init_once
,我们继续查看其实现:
_dispatch_root_queue_init_pthread_pool
:判断当前可调度线程池的状况;pthread_workqueue_config
:工作队列的配置信息;
但是我们实在是不能分辨到底该如何继续往下分析,那么我们可以采用倒推的方法,先查看异步函数
的堆栈:
在对战信息中_dispatch_worker_thread2
刚好在工作队列的配置信息里边,那么我们顺着_dispatch_worker_thread2
继续分析:
这里调用了_dispatch_root_queue_drain
,进入其实现:
但是在这个方法中并没有任何地方调用_dispatch_queue_override_invoke
,那么一定是方法体内的某一个方法调用了_dispatch_queue_override_invoke
,经过查看我们发现是_dispatch_continuation_pop_inline
调用了_dispatch_continuation_invoke_inline
,然后调用了_dispatch_client_callout
,而_dispatch_client_callout
最终实现为:
源码分析甚是枯燥,还需多一份耐心……