这是我参与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最终实现为:
源码分析甚是枯燥,还需多一份耐心......