iOS底层原理23:GCD分析(上)

这是我参与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是一个并发队列,而队列中任务的执行是一个耗时操作;所以15先打印;
  • 接下来执行dispatch_async中的任务,先打印2
  • dispatch_sync是一个同步函数,所以dispatch_sync会阻塞任务,打印3;
  • 最后打印4

所以最终打印: 1 5 2 3 4

接下来,我们对代码稍作修改:

最终执行代码会崩溃;

解析

  • 15依然先打印;
  • 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_queuelabel字符串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,并且传递了labelattrDISPATCH_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)width1,那么就是串行队列,否则就是并发队列,这跟我们上文分析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_INIT17:

根据注释,我们可以看出,这个宏定义标识了不同的队列,主队列时其值为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_explicitC++ 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_infodqa进行面向对象封装;

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
复制代码

此处namedispatch_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,点击进入这个方法的实现:

此方法中和ctxtfunc有关的方法有两个_dispatch_sync_function_invoke_dispatch_sync_invoke_and_complete_recurse,分别下符号断点,继续运行:

点击进入_dispatch_sync_function_invoke

点击进入_dispatch_sync_function_invoke_inline:

此处跟ctxtfunc有关的只有_dispatch_client_callout,点击进入方法:

最终在这个地方执行了回调;那么究竟是不是这样呢?我们在工程中打印一下堆栈验证一下:

验证结果与我们分析的结果一致;

异步流程

我们来看一个同步GCD函数

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"GCD 函数分析");
});
复制代码

其函数声明如下:

我们在源码中搜索它的实现:

这里边和work有关的方法是_dispatch_continuation_init,点击进入其实现:

这里边与ctxtwork有关的函数是_dispatch_continuation_init_f,点击进入这个方法:

在这里ctxtf分别赋值给了dcdc_ctxtdc_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_pushdx_vtable(x)->dq_push(x, y, z)此处是一个函数的调用,那么一定存在这对应函数的赋值操作:

针对队列的不同,赋值不同的值,全局并发队列赋值情况为_dispatch_root_queue_push,搜索定位:

此处与qos有关的是_dispatch_root_queue_push_override,但由于宏定义HAVE_PTHREAD_WORKQUEUE_QOS0,所以此函数并不执行,将会执行(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最终实现为:

源码分析甚是枯燥,还需多一份耐心……

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享