前言
-
上篇文章讲了多线程
GCD的队列与函数搭配,以及队列的底层的相关内容,那么函数的执行又是怎样的呢,本文将针对函数进行深入分析 -
这里函数的执行也就是
block块的调用,我们主要探索的是block什么时候调用,按照开线程,可以分为以下几种情况,然后结合源码 libdispatch-1271.120.2来分析
同步函数
- 我们知道同步函数是在当前线程执行,不会开辟线程,所以就先从同步
dispatch_sync开始入手,然后再查看里面队列的区分 - 在源码中搜索
dispatch_sync(,得到其源码如下:
dispatch_sync
void
dispatch_sync(dispatch_queue_t dq, dispatch_block_t work)
{
uintptr_t dc_flags = DC_FLAG_BLOCK;
if (unlikely(_dispatch_block_has_private_data(work))) {
return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
}
_dispatch_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
}
work就是我们需要研究的block,排除unlikely,然后就把目标放在_dispatch_sync_f方法,先来看看参数_dispatch_Block_invoke:
#ifdef __BLOCKS__
#define _dispatch_Block_invoke(bb) \
((dispatch_function_t)((struct Block_layout *)bb)->invoke)
- 这里的
bb是传入的work,也就是block,然后_dispatch_Block_invoke(bb)就是让block调用invoke,也就是block调用
_dispatch_sync_f
- 再看
_dispatch_sync_f方法:
static void
_dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func,
uintptr_t dc_flags)
{
_dispatch_sync_f_inline(dq, ctxt, func, dc_flags);
}
- 这里
ctxt就是要研究的block,func是方法的调用_dispatch_Block_invoke
_dispatch_sync_f_inline
- 然后再看
_dispatch_sync_f_inline方法:
static inline void
_dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt,
dispatch_function_t func, uintptr_t dc_flags)
{
if (likely(dq->dq_width == 1)) { // 串行
return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags); // 栅栏函数
}
if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
}
dispatch_lane_t dl = upcast(dq)._dl;
// Global concurrent queues and queues bound to non-dispatch threads
// always fall into the slow case, see DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
if (unlikely(!_dispatch_queue_try_reserve_sync_width(dl))) {
return _dispatch_sync_f_slow(dl, ctxt, func, 0, dl, dc_flags); // 先来
}
if (unlikely(dq->do_targetq->do_targetq)) {
return _dispatch_sync_recurse(dl, ctxt, func, dc_flags);
}
_dispatch_introspection_sync_begin(dl);
_dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG(
_dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags)));
}
- 在上节课,我们分析了当
dq_width值为1时是串行队列,也就是这里串行会走_dispatch_barrier_sync_f方法
同步函数串行队列
_dispatch_barrier_sync_f
- 进入
_dispatch_barrier_sync_f方法的实现:
static void
_dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt,
dispatch_function_t func, uintptr_t dc_flags)
{
_dispatch_barrier_sync_f_inline(dq, ctxt, func, dc_flags);
}
- 然后再进入
_dispatch_barrier_sync_f_inline方法:
- 此刻有三个方法涉及
func和ctxt,该从哪个开始看呢?我们可以通过断点再结合符号断点来探索:
- 运行之前先去掉符号断点,避免系统执行线程时造成干扰,当断点进入
viewController的断点后,再打开符号断点,然后继续执行,结果进入了_dispatch_lane_barrier_sync_invoke_and_complete方法:
_dispatch_lane_barrier_sync_invoke_and_complete
- 首先来看下方法的参数,
func是参数,但后面也有个DISPATCH_TRACE_ARG(void *dc)参数,但参数和参数之间怎么没有,隔开?,再来看看DISPATCH_TRACE_ARG是个什么鬼,搜索发现它有两个:
#define DISPATCH_TRACE_ARG(arg)
#define DISPATCH_TRACE_ARG(arg) , arg
- 有参数时,它带上了
, 再加参数,没有参数时代表空,这写的也太鸡贼了
_dispatch_sync_function_invoke_inline
- 再接着看
_dispatch_sync_function_invoke_inline函数:
static inline void
_dispatch_sync_function_invoke_inline(dispatch_queue_class_t dq, void *ctxt,
dispatch_function_t func)
{
dispatch_thread_frame_s dtf;
_dispatch_thread_frame_push(&dtf, dq);
_dispatch_client_callout(ctxt, func);
_dispatch_perfmon_workitem_inc();
_dispatch_thread_frame_pop(&dtf);
}
然后就定位到_dispatch_client_callout函数:
_dispatch_client_callout
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
_dispatch_get_tsd_base();
void *u = _dispatch_get_unwind_tsd();
if (likely(!u)) return f(ctxt);
_dispatch_set_unwind_tsd(NULL);
f(ctxt);
_dispatch_free_unwind_tsd();
_dispatch_set_unwind_tsd(u);
}
- 这里不管
likely(!u)条件是否限制,都会执行f(ctxt),也就是block调用
代码验证
- 在刚才的符号断点处继续执行,就来到了同步函数
block里面,再bt打印: - 堆栈的打印结果和分析结果一致。
死锁
- 我们知道同步串行会出现死锁的情况,这个过程是怎样的呢?
- 先来定义个死锁,然后运行后看堆栈:
_dispatch_sync_f_slow我们在上面分析时见过,说明进入_dispatch_barrier_sync_f_inline方法后,如果是死锁的情况,就会进入_dispatch_sync_f_slow方法
_dispatch_sync_f_slow
- 根据刚才的堆栈打印,会到了
__DISPATCH_WAIT_FOR_QUEUE__方法
__DISPATCH_WAIT_FOR_QUEUE__
于是就出现了报错:同步方法在当前线程已经被队列调用,也就是需要调用的线程,以及被当前的线程调用,就形成了相互等待
- 再来看看判断死锁的条件
_dq_state_drain_locked_by函数
_dq_state_drain_locked_by
static inline bool
_dq_state_drain_locked_by(uint64_t dq_state, dispatch_tid tid)
{
return _dispatch_lock_is_locked_by((dispatch_lock)dq_state, tid);
}
- 继续查看
_dispatch_lock_is_locked_by
_dispatch_lock_is_locked_by
static inline bool
_dispatch_lock_is_locked_by(dispatch_lock lock_value, dispatch_tid tid)
{
// equivalent to _dispatch_lock_owner(lock_value) == tid
return ((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0;
}
#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc)
DLOCK_OWNER_MASK是个很大的值,也就是只有lock_value和tid相同时,与上DLOCK_OWNER_MASK的结果才会为0,也即是当前队列要等待的和需要执行的队列要等待的一样时,会出现你要等我,我要等你,进而产生死锁
同步函数并行队列
- 在上面
_dispatch_sync_f_inline函数,如果dq_width不为1,就是并发队列,后面有三处使用了func,对三个函数使用符号断点,然后定义同步并发队列运行:
- 最终执行
_dispatch_sync_f_slow方法,这个方法在死锁的时候进来过
_dispatch_sync_f_slow
- 方法里有两处用到了
func,分别是_dispatch_sync_function_invoke和_dispatch_sync_invoke_and_complete_recurse,再下符号断点确认,最终进入_dispatch_sync_function_invoke方法
_dispatch_sync_function_invoke
static void
_dispatch_sync_function_invoke(dispatch_queue_class_t dq, void *ctxt,
dispatch_function_t func)
{
_dispatch_sync_function_invoke_inline(dq, ctxt, func);
}
- 这次又来到了
_dispatch_sync_function_invoke_inline方法,和串行队列一样
_dispatch_sync_function_invoke_inline
static inline void
_dispatch_sync_function_invoke_inline(dispatch_queue_class_t dq, void *ctxt,
dispatch_function_t func)
{
dispatch_thread_frame_s dtf;
_dispatch_thread_frame_push(&dtf, dq);
_dispatch_client_callout(ctxt, func);
_dispatch_perfmon_workitem_inc();
_dispatch_thread_frame_pop(&dtf);
}
- 最后执行
_dispatch_client_callout
_dispatch_client_callout
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
_dispatch_get_tsd_base();
void *u = _dispatch_get_unwind_tsd();
if (likely(!u)) return f(ctxt);
_dispatch_set_unwind_tsd(NULL);
f(ctxt);
_dispatch_free_unwind_tsd();
_dispatch_set_unwind_tsd(u);
}
- 最后通过
f(ctxt)进行调用
同步函数总结
- 同步函数执行流程如下:
异步函数
- 再来看看异步函数的流程,搜索
dispatch_async
dispatch_async
void
dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
{
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME;
dispatch_qos_t qos;
qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
- 异步的情况明显要复杂些,首先要调用
_dispatch_continuation_alloc方法开辟一条线程
_dispatch_continuation_alloc
static inline dispatch_continuation_t
_dispatch_continuation_alloc(void)
{
dispatch_continuation_t dc =
_dispatch_continuation_alloc_cacheonly(); // 先从线程池获取线程可执行任务的线程
if (unlikely(!dc)) { // 如果没有,就从堆中开辟一个空间创建一条
return _dispatch_continuation_alloc_from_heap();
}
return dc;
}
- 先调用
_dispatch_continuation_alloc_cacheonly方法从线程池获取可执行任务的线程
_dispatch_continuation_alloc_cacheonly
static inline dispatch_continuation_t
_dispatch_continuation_alloc_cacheonly(void)
{
dispatch_continuation_t dc = (dispatch_continuation_t)
_dispatch_thread_getspecific(dispatch_cache_key);
if (likely(dc)) {
_dispatch_thread_setspecific(dispatch_cache_key, dc->do_next);
}
return dc;
}
- 根据
key获取可执行的线程,如果获取到就调用dc->do_next给他一个可执行下个任务的状态,set和get方法如下: - 可以看到底层的实现都是
pthread相关操作,说明线程的操作都是pthread封装的 - 没有找到线程就执行
_dispatch_continuation_alloc_from_heap方法
_dispatch_continuation_alloc_from_heap
dispatch_continuation_t
_dispatch_continuation_alloc_from_heap(void)
{
_dispatch_continuation_alloc_once(); //单例
#if DISPATCH_ALLOCATOR
if (_dispatch_use_dispatch_alloc)
return _dispatch_alloc_continuation_alloc();
#endif
#if DISPATCH_CONTINUATION_MALLOC
return _dispatch_malloc_continuation_alloc();
#endif
}
- 这里主要是通过单例子去创建线程
_dispatch_continuation_async
- 系统调用
_dispatch_continuation_init函数,将block,dq(队列)和dc(线程)生成一个dispatch_qos_t类型的qos,然后调用_dispatch_continuation_async:
static inline void
_dispatch_continuation_async(dispatch_queue_class_t dqu,
dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
{
#if DISPATCH_INTROSPECTION
if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
_dispatch_trace_item_push(dqu, dc);
}
#else
(void)dc_flags;
#endif
return dx_push(dqu._dq, dc, qos);
}
- 这里的
dx_push是个宏定义
dx_push
#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
- 由于我们要研究的是
block调用,也就是dqu相关的,所以就看dq_push
dq_push
通过搜索发现`dq_push有很多,根据队列来区分
- 先来看串行的情况
异步主队列_dispatch_main_queue_push
- 异步主队列的
dp_push类型是_dispatch_main_queue_push,源码如下:
void
_dispatch_main_queue_push(dispatch_queue_main_t dq, dispatch_object_t dou,
dispatch_qos_t qos)
{
// Same as _dispatch_lane_push() but without the refcounting due to being
// a global object
if (_dispatch_queue_push_item(dq, dou)) {
return dx_wakeup(dq, qos, DISPATCH_WAKEUP_MAKE_DIRTY);
}
qos = _dispatch_queue_push_qos(dq, qos);
if (_dispatch_queue_need_override(dq, qos)) {
return dx_wakeup(dq, qos, 0);
}
}
- 这里返回的都是
dx_wakeup函数的调用,来看下dx_wakeup
dx_wakeup
#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
- 根据要研究的是
x和y,再来看dq_wakeup,选择主队列的赋值_dispatch_main_queue_wakeup,它的实现如下:
_dispatch_main_queue_wakeup
void
_dispatch_main_queue_wakeup(dispatch_queue_main_t dq, dispatch_qos_t qos,
dispatch_wakeup_flags_t flags)
{
#if DISPATCH_COCOA_COMPAT
if (_dispatch_queue_is_thread_bound(dq)) {
return _dispatch_runloop_queue_wakeup(dq->_as_dl, qos, flags);
}
#endif
return _dispatch_lane_wakeup(dq, qos, flags);
}
- 根据下符号断点得知走
_dispatch_runloop_queue_wakeup
_dispatch_runloop_queue_wakeup
void
_dispatch_runloop_queue_wakeup(dispatch_lane_t dq, dispatch_qos_t qos,
dispatch_wakeup_flags_t flags)
{
if (unlikely(_dispatch_queue_atomic_flags(dq) & DQF_RELEASED)) {
// <rdar://problem/14026816>
return _dispatch_lane_wakeup(dq, qos, flags);
}
if (flags & DISPATCH_WAKEUP_MAKE_DIRTY) {
os_atomic_or2o(dq, dq_state, DISPATCH_QUEUE_DIRTY, release);
}
if (_dispatch_queue_class_probe(dq)) {
return _dispatch_runloop_queue_poke(dq, qos, flags);
}
qos = _dispatch_runloop_queue_reset_max_qos(dq);
if (qos) {
mach_port_t owner = DISPATCH_QUEUE_DRAIN_OWNER(dq);
if (_dispatch_queue_class_probe(dq)) {
_dispatch_runloop_queue_poke(dq, qos, flags);
}
_dispatch_thread_override_end(owner, dq);
return;
}
if (flags & DISPATCH_WAKEUP_CONSUME_2) {
return _dispatch_release_2_tailcall(dq);
}
}
- 这里又出现两个相关函数,再下符号断点确定走
_dispatch_runloop_queue_poke
_dispatch_runloop_queue_poke
static void
_dispatch_runloop_queue_poke(dispatch_lane_t dq, dispatch_qos_t qos,
dispatch_wakeup_flags_t flags)
{
// it's not useful to handle WAKEUP_MAKE_DIRTY because mach_msg() will have
// a release barrier and that when runloop queues stop being thread-bound
// they have a non optional wake-up to start being a "normal" queue
// either in _dispatch_runloop_queue_xref_dispose,
// or in _dispatch_queue_cleanup2() for the main thread.
uint64_t old_state, new_state;
if (dx_type(dq) == DISPATCH_QUEUE_MAIN_TYPE) {
dispatch_once_f(&_dispatch_main_q_handle_pred, dq,
_dispatch_runloop_queue_handle_init);
}
...
}
- 这里就能看到
主线程的判断,dispatch_once_f是一个单例方法,而第三个参数_dispatch_runloop_queue_handle_init就是要回调的方法,实现如下:
- 这里主要是对
dq的相关处理,我们需要关注的就是它什么时候被调用,搜索发现在_dispatch_runloop_root_queue_create_4CF方法中被调用
_dispatch_runloop_root_queue_create_4CF
- 然后根据反推法发现会走到
_dispatch_main_queue_callback_4CF方法
_dispatch_main_queue_callback_4CF
- 然后在单例方法
dispatch_once_f中找到_dispatch_once_callout进行回调
_dispatch_once_callout
static void
_dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
dispatch_function_t func)
{
_dispatch_client_callout(ctxt, func);
_dispatch_once_gate_broadcast(l);
}
- 最后走到了
_dispatch_client_callout函数,进而进行block回调
异步串行队列_dispatch_lane_push
- 这里会走到
dx_wakeup->dq_wakeup中的_dispatch_lane_wakeup类型
_dispatch_lane_wakeup
- 然后会进入
_dispatch_queue_wakeup方法
_dispatch_queue_wakeup
- 这里第一个参数已经改变,然后会进入
_dispatch_queue_push_queue方法
_dispatch_queue_push_queue
- 根据反推法得知会进入
_dispatch_event_loop_poke方法
_dispatch_event_loop_poke
- 这里会进入
_dispatch_trace_item_push方法
_dispatch_trace_item_push
- 根据反推法会进入
_dispatch_trace_continuation方法
_dispatch_trace_continuation
- 这是个宏定义,我们只需要找到相关
func就可以,也就是_dispatch_lane_invoke
_dispatch_lane_invoke
- 由于
_dispatch_queue_class_invoke是根据第五个参数invoke来获取tq,所以我们只需研究_dispatch_lane_invoke2
_dispatch_lane_invoke2
- 根据前面我们得知
dq_width = 1时是串行,所以我们将目标放在_dispatch_lane_serial_drain函数
_dispatch_lane_serial_drain
- 在查看里面方法
_dispatch_lane_drain
_dispatch_lane_drain
- 经过分析会走到
_dispatch_continuation_pop_inline方法
_dispatch_continuation_pop_inline
- 在该函数里最终会走进
_dispatch_continuation_invoke_inline方法
_dispatch_continuation_invoke_inline
- 最终在里面找到了
_dispatch_client_callout函数
异步全局队列_dispatch_root_queue_push
源码如下:
- 根据分析,代码会走
_dispatch_root_queue_push_inline方法
_dispatch_root_queue_push_inline
static inline void
_dispatch_root_queue_push_inline(dispatch_queue_global_t dq,
dispatch_object_t _head, dispatch_object_t _tail, int n)
{
struct dispatch_object_s *hd = _head._do, *tl = _tail._do;
if (unlikely(os_mpsc_push_list(os_mpsc(dq, dq_items), hd, tl, do_next))) {
return _dispatch_root_queue_poke(dq, n, 0);
}
}
- 然后会走
_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
static inline void
_dispatch_root_queues_init(void)
{
dispatch_once_f(&_dispatch_root_queues_pred, NULL,
_dispatch_root_queues_init_once);
}
- 这是个单例下面再讲它的原理,主要的回调函数封装在
_dispatch_root_queues_init_once里
_dispatch_root_queues_init_once
- 这里主要是创建一个
pthread实例,然后将任务_dispatch_worker_thread2交给它处理
_dispatch_worker_thread2
- 这里主要是获取
root_queue,然后再调用_dispatch_root_queue_drain方法
_dispatch_root_queue_drain
- 然后再进入
_dispatch_continuation_pop_inline方法
_dispatch_continuation_pop_inline
- 根据反推法,得知这里走了
dx_invoke,根据dx_invoke找到do_invoke
_dispatch_async_redirect_invoke
- 然后会进入
_dispatch_continuation_pop方法
_dispatch_continuation_pop
- 方法里又回到
_dispatch_continuation_pop_inline函数,此时进入函数会执行_dispatch_continuation_invoke_inline方法
_dispatch_continuation_invoke_inline
- 于是就找到了
_dispatch_client_callout函数,也就是最终的回调函数处
线程处理
- 这里的
do-while是处理线程的开辟,首先需要拿到remaining的值为1,然后和can_request做对比,如果remaining大于can_request,则把can_request的值给remaining防止出错,当remaining为0时,说明线程池已经满了 dgq_thread_pool_size是线程池的大小,根据搜索发现初始值为1
最大并发数
- 搜索发现
dgq_thread_pool_size的值最大为DISPATCH_WORKQ_MAX_PTHREAD_COUNT
#define DISPATCH_WORKQ_MAX_PTHREAD_COUNT 255
结论:线程池中理论最大的并发数是
255但占用的内存是多少呢?
占用内存
根据Thread Costs中相关说明,辅助线程最小堆栈大小为16KB ,并且大小必须是4KB的倍数
- 如果在一定的内存内,如果创建的线程内存越大,则能开辟的线程越少
异步并发队列_dispatch_lane_concurrent_push
- 根据符号断点得知会执行
_dispatch_continuation_redirect_push方法
_dispatch_continuation_redirect_push
- 这里又进入了
dx_push方法,但此时dq已经变了,会走_dispatch_root_queue_push,之后的流程和异步全局队列一样.
单例
GCD的单例我们比较常用,写法也比较简单dispatch_once ...,我们来看看它的底层实现:
dispatch_once
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
- 底层只有控制参数
val和回调方法block,原理就是通过改变变量来达到核心方法只执行一次的效果。
dispatch_once_f
- 再来看看
dispatch_once_f函数:
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
dispatch_once_gate_t l = (dispatch_once_gate_t)val;
#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
if (likely(v == DLOCK_ONCE_DONE)) { // 判断是否执行过
return;
}
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
if (likely(DISPATCH_ONCE_IS_GEN(v))) { // 判断是否执行过
return _dispatch_once_mark_done_if_quiesced(l, v);
}
#endif
#endif
if (_dispatch_once_gate_tryenter(l)) { // 原子加锁,保证线程安全
return _dispatch_once_callout(l, ctxt, func);
}
return _dispatch_once_wait(l);
}
- 这里先将
val强转成dispatch_once_gate_t类型的l - 然后再根据
&l->dgo_once判断是否已经执行过一次,如果已经执行过了,就直接返回 - 如果没用执行过就来到
_dispatch_once_gate_tryenter:static inline bool _dispatch_once_gate_tryenter(dispatch_once_gate_t l) { return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED, (uintptr_t)_dispatch_lock_value_for_self(), relaxed); // 原子操作加锁 }- 该方法是对自己进行加锁,并判断是否成功
_dispatch_client_callout
- 如果加锁成功了,则执行
_dispatch_once_callout函数:static void _dispatch_once_callout(dispatch_once_gate_t l, void *ctxt, dispatch_function_t func) { _dispatch_client_callout(ctxt, func); // 函数回调 _dispatch_once_gate_broadcast(l); }- 这里
_dispatch_client_callout是进行函数回调 _dispatch_once_gate_broadcast函数是进行标记:
- 这里
_dispatch_once_mark_done的实现如下:
static inline uintptr_t _dispatch_once_mark_done(dispatch_once_gate_t dgo) { return os_atomic_xchg(&dgo->dgo_once, DLOCK_ONCE_DONE, release); // 将状态标记成 DLOCK_ONCE_DONE }_dispatch_once_mark_done方法主要是将状态标记成DLOCK_ONCE_DONE,等下次执行单例时方便判断
- 这里
_dispatch_once_wait
- 如果没用执行,然后也被锁锁住了,就会执行
_dispatch_once_wait方法进行等待,等待开锁
总结
- 相比同步和异步,异步函数的底层分析要难得多,需要花很多时间去分析去跟流程,主要是用
符号断点和反推法进行分析。 - 如果流程有错误,欢迎指正~