iOS同步函数与异步函数底层原理分析

641 阅读5分钟

这是我参与8月更文挑战的第18天,活动详情查看: 8月更文挑战

同步函数任务同步性

_dispatch_sync_f_inline

我们在之前的文章中分析到同步函数dispatch_sync-->_dispatch_sync_f-->_dispatch_sync_f_inline会执行到_dispatch_sync_f_inline方法,那么我们就在方法内继续分析;

之前的分析中,我们分析出,此方法内在串行队列时的_dispatch_barrier_sync_f方法在某种条件下会导致死锁;那么并发队列是如何处理的呢?

这个时候,我们无从下手,下边那么多方法,我们并不清楚并发队列会走哪一个方法,这个时候我们可以进行符号断点:

新建一个工程,我们用如下代码来演示一个并发队列:

然后依次添加以下后边的四个符号断点:

  • _dispatch_sync_f_slow
  • _dispatch_sync_recurse
  • _dispatch_introspection_sync_begin
  • _dispatch_sync_invoke_and_complete

继续执行:

说明全局并发队列进入方法_dispatch_sync_f_slow:

进入_dispatch_sync_f_slow方法;

_dispatch_sync_f_slow

这个时候,我们依然无法判断,在_dispatch_sync_f_slow方法中会如何执行,那么我们如法炮制,继续添加符号断点:

我们在上文探索串行队列的时候知道,最终串行队列进入__DISPATCH_WAIT_FOR_QUEUE__,所以__DISPATCH_WAIT_FOR_QUEUE__的符号断点我们就不用在研究了,我们只需要分析其它的符号断点就可以了:

  • _dispatch_sync_function_invoke
  • _dispatch_trace_item_push
  • _dispatch_sync_complete_recurse
  • _dispatch_introspection_sync_begin
  • _dispatch_trace_item_pop
  • _dispatch_sync_invoke_and_complete_recurse

继续运行代码,发现进入符号断点_dispatch_sync_function_invoke

接下来进入_dispatch_sync_function_invoke方法内部;

_dispatch_sync_function_invoke

继续查看_dispatch_sync_function_invoke_inline方法;

_dispatch_sync_function_invoke_inline

继续添加符号断点:

  • _dispatch_thread_frame_push
  • _dispatch_client_callout
  • _dispatch_perfmon_workitem_inc
  • _dispatch_thread_frame_pop

继续执行发现进入符号断点_dispatch_client_callout

继续查看_dispatch_client_callout方法;

_dispatch_client_callout

其实现如下:

最终,f(ctxt)任务执行;

异步函数分析

异步函数的分析,我们以dispatch_async为切入点分析:

dispatch_async

任务被包装在qos中;

然后进入_dispatch_continuation_async方法;

_dispatch_continuation_async

我们又看到了dx_push,在之前的分析中,我们已经知道了dx_push是一个宏定义:

#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)

说白了,dx_push最终调用的是dq_push

dx_push -> dq_push

dq_push会根据队列的不同,而进行不同的赋值

这里我们以并发队列为例进行探索分析:

并发队列中,dq_push被赋值为:_dispatch_lane_concurrent_push;

_dispatch_lane_concurrent_push

  • _dispatch_object_is_barrier是一个栅栏函数,关于什么是栅栏函数我们后边会继续讲解;

_dispatch_lane_concurrent_push的方法实现中,我们发现调用了_dispatch_lane_push,而在串行队列的时候,dq_push也是被赋值为_dispatch_lane_push;

也就是说,不管是串行队列还是并发队列最终都会调用_dispatch_lane_push方法;

_dispatch_lane_push

这个方法实现相对复杂,有多个return,那么我们就需要用到符号断点进行分析,到底接下来会进入哪个方法;添加以下符号断点:

  • _dispatch_lane_push_waiter
  • dx_wakeup

这里我们省去符号断点的流程,直接进入dx_wakeup方法;

dx_wakeup

#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)

宏定义dx_wakeup最终调用的其实是dq_wakeup,那么dq_wakeup是什么呢?

dq_wakeup

我们发现,在并发队列dq_wakeup赋值的是_dispatch_lane_wakeup;

不管是串行队列还是并发队列dq_wakeup赋值的都是_dispatch_lane_wakeup方法

_dispatch_lane_wakeup

此处又有两个return,其实按照逻辑,因为我们没有使用栅栏函数,所以很明显并不会走_dispatch_lane_barrier_complete流程,不过为了严谨,我们依然添加符号断点:

  • _dispatch_lane_barrier_complete
  • _dispatch_queue_wakeup

继续运行工程,发现符号断点_dispatch_queue_wakeup被执行:

接下来将会执行_dispatch_queue_wakeup方法;

_dispatch_queue_wakeup

方法复杂,我们依然需要借助符号断点的方式进行分析,添加符号断点:

  • _dispatch_lane_class_barrier_complete
  • _dispatch_queue_push_queue
  • _dispatch_queue_wakeup_with_override
  • _dispatch_release_2_tailcall

这个地方会频繁的跳_dispatch_lane_push_dispatch_queue_wakeup,然后最终跳到_dispatch_lane_class_barrier_complete(这里也有可能是后台任务的调用导致的,这个时候,我们只需要把其他任务注释掉,重新运行项目,重新来一遍就可以了)

接下来进入_dispatch_lane_class_barrier_complete 方法;

_dispatch_lane_class_barrier_complete

这个时候就已经很复杂,我们以全局并发队列dq_push继续分析:

全局并发队列dq_push指向了_dispatch_root_queue_push;

_dispatch_root_queue_push

这里有两种情况,一种会返回_dispatch_root_queue_push_override,另一种会调用_dispatch_root_queue_push_inline,我们添加这两个符号断点

_dispatch_root_queue_push_override

我们发现_dispatch_root_queue_push_override方法中,最后还是调用了_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_queues_init函数,就是为了让函数_dispatch_root_queues_init_once只调用一次;

那么_dispatch_root_queues_init_once做了什么事情呢?

_dispatch_root_queues_init_once

此函数将会进入block的堆栈,进行正向反向的对接;

  • cfg.workq_cb = _dispatch_worker_thread2;:workq_cb被标记为_dispatch_worker_thread2;

那么这个_dispatch_worker_thread2在哪里进行调用了呢?

GCD任务执行的堆栈中我们找到了_dispatch_worker_thread2;而_dispatch_worker_thread2是由底层_pthread_wqthread调起的;

接下来,我们回到_dispatch_root_queue_poke_slow函数的实现中继续分析:

  • _dispatch_debug_root_queue任务的一些debug的处理;
  • _dispatch_trace_runtime_event:运行时runtime的一些处理;
  • 如果是全局并发队列将会执行_pthread_workqueue_addthreads,创造线程进行处理:
  • 如果是一个普通的并发队列,那么将会进行一个do-while循环:

那么这个do-while做了什么事情呢?

我们来看一下while的条件:

  • dq:是我们的并发队列;
  • dgq_thread_pool_size:在并发队列时,被标记为1;之后会不断根据任务量进行赋值;

do中主要是进行一些线程池的处理:

  • remaining当前空余的数量;是从上一个方法传进来的,通过方法调用往回查找发现它是1;这是因为全局并发队列进来的一次,只创建一个线程;当remaining进行--remaining之后,最终为0,标明当前线程池满员;
  • can_request能够请求到的数量;
  • floor是从之前的方法中传过来的,我们可以通过方法调用往回查找发现它是0;(一般情况下,floor01
  • t_count是通过os_atomic_load2o获取到的,并且和dgq_thread_pool_size有关系;dgq_thread_pool_size会不断被赋值(通过os_atomic_inc2o进行++操作不断变大)
  • dgq_thread_pool_size线程池大小;他会被赋值为thread_pool_size,而thread_pool_size的大小等于DISPATCH_WORKQ_MAX_PTHREAD_COUNT也就是255
#ifndef DISPATCH_WORKQ_MAX_PTHREAD_COUNT
#define DISPATCH_WORKQ_MAX_PTHREAD_COUNT 255
#endif