iOS GCD底层分析(2)--同步异步函数、死锁、GCD单例

前言

上一篇文章iOS GCD底层分析(1)留下了四个问题,分别是:

  • 死锁底层是怎么样子产生的?
  • 如果是异步函数,线程是怎样子创建的?
  • 底层通过_dispatch_worker_thread2方法完成任务的回调执行,那么触发调用的位置在哪?
  • 单例的底层原理是什么?

准备工作

  • libdispatch.dylib
  • iOS GCD底层分析(1)

1. 同步函数

上一篇文章中分系同步函数时进入了_dispatch_sync_f_inline的流程,如下:

_dispatch_sync_f_inline流程

通过下符号断点,我们可以确定如果队列为串行队列,会走到_dispatch_barrier_sync_f流程中,这与我们的分析也是一致的,因为这里dq_width=1,所以是串行队列。如果是并发队列,则会走到_dispatch_sync_f_slow

注意:想要知道同步函数还有异步函数的之前的底层流程,需要看前面一篇的内容哦,这里就不再写一遍了

1.1 死锁

进入_dispatch_barrier_sync_f方法,分析其中的流程如下:

_dispatch_barrier_sync_f

继续进入_dispatch_barrier_sync_f_inline方法,发现起内部有一个判断,是用来判断当前队列是否等待或者挂起,如下:
_dispatch_barrier_sync_f_inline

进入_dispatch_queue_try_acquire_barrier_sync判断方法,如下:
_dispatch_queue_try_acquire_barrier_sync

_dispatch_queue_try_acquire_barrier_sync_and_suspend

在该流程中会对队列的状态进行判断放弃底层的执行流程,也就是让队列不再调度别的任务,返回控制处理。如果当前队列处于挂起或者阻塞状态会执行_dispatch_sync_f_slow方法(和同步函数并发队列执行的方法一样)。

疑问:那么_dispatch_sync_f_slow方法中,死锁的反馈在哪?
在找出答案之前,我们冼运行一个死锁的案例,查看汇编,如下:

    // 主线程
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"1");
    });

当前案例会产生死锁,打开汇编断点,查看:

汇编断点

结果跟我们的分析是一直的,死锁最终调用了_dispatch_sync_f_slow方法,而真正导致死锁的位置是__DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq);,见下图:
死锁的触发点

继续进入__DISPATCH_WAIT_FOR_QUEUE__方法,查看源码如下:
__DISPATCH_WAIT_FOR_QUEUE__

首先会获取当前要使用队列的状态,然后调用_dq_state_drain_locked_by方法和当前的队列进行比较,满足一定条件即视为死锁。进入_dq_state_drain_locked_by方法,查看其判断逻辑,见下图:
_dq_state_drain_locked_by

_dispatch_lock_is_locked_by

DLOCK_OWNER_MASK是一个很的数,说明当lock_value ^ tid = 0时,才会返回0,也就是说此时使用的队列和当前等待的队列同一个队列

 #define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc)

死锁总结:
当前队列处于等待状态,而又有新的任务过来需要使用这个队列去调度,这样产生了矛盾,进入相互等待状态,进而产生死锁。

2. 异步函数

上一篇文章中,我们通过dx_push定位到,底层为不同类型的队列提供不同的调用入口,比如全局并发队列会调用_dispatch_root_queue_push方法。通过下符号断点,跟踪源码,最终定位到一个重要的方法_dispatch_root_queue_poke_slow,进入此方法如下:

_dispatch_root_queue_poke_slow

前面也分析了_dispatch_root_queues_init方法,使用了单例。在该方法中,采用单例的方式进行了线程池的初始化处理工作队列的配置工作队列的初始化等工作。同时这里有一个关键的设置,执行函数的设置,也就是将任务执行的函数被统一设置成了_dispatch_worker_thread2。见下图:
_dispatch_worker_thread2
]
这里的调用执行是通过workloop工作循环调用起来的,也就是说并不是及时调用的,而是通过os完成调用,说明异步调用的关键是在需要执行的时候能够获取对应的方法,进行异步处理,而同步函数是直接调用

疑问:_dispatch_worker_thread2在哪里调用呢?继续深入分析_dispatch_root_queue_poke_slow方法

全局队列创建线程

由上图可知,如果是全局队列,此时会创建线程进行执行任务

线程池进行处理,从线程池中获取线程,执行任务,同时判断线程池的变化。见下图:

线程池的一系列处理

remaining可以理解为当前可用线程数,当可用线程数等于0时,线程池已满pthread pool is full,直接return。底层通过pthread完成线程的开辟,见下图:
pthread开辟线程

也就是说_dispatch_worker_thread2是通过pthread完成oc_atmoic原子触发!!。

r = _pthread_workqueue_init(_dispatch_worker_thread2,
                offsetof(struct dispatch_queue_s, dq_serialnum), 0);

2.1 能开辟多少线程

通过解读前面的源码,发现队列线程池的大小为:dgq_thread_pool_sizedgq_thread_pool_size被赋值为:thread_pool_size,见下图:

初始化线程池

thread_pool_size的初始值为:DISPATCH_WORKQ_MAX_PTHREAD_COUNT。全局搜索,定义如下:
thread_pool_size初始值

255表示理论上线程池的最大数量。但是实际能开辟多少呢,这个不确定。在苹果官方完整Thread Management中,有相关的说明,辅助线程的最小允许堆栈大小为 16 KB,并且堆栈大小必须是 4 KB 的倍数。见下图:


也就是说,一个辅助线程的栈空间是512KB,而一个线程所占用的最小空间是16KB,也就是说栈空间一定的情况下,开辟线程所需的内存越大,所能开辟的线程数就越小。针对一个4GB内存的iOS真机来说,内存分为内核态用户态,如果内核态全部用于创建线程,也就是1GB的空间,也就是说最多能开辟1024MB / 16KB个线程。但是着只是理论值。

3. GCD单例

3.1 单例的使用案例

只执行一次!

   static dispatch_once_t token;

   dispatch_once(&token, ^{
       // code
   });

3.2 单例的定义

libdispatch.dylib源码中全局搜索_dispatch_once,见下图:

dispatch_once定义

这里只分析dispatch_once,进入dispatch_once实现,见下图:
dispatch_once内部实现

最终会调用dispatch_once_f,进入此方法查看源码实现见下图:
dispatch_once_f实现源码

初步流程分析:
首先要创建一个标识,如果标识已经被特殊标记,说明已经执行过了;如果没有被特殊标记过,说明可以进行执行。同时为了保证线程安全,在关键流程中需要加锁

整个创建流程分析:
1.首先会对传入的val进行数据包装,包装成l,这个val就是外面创建的oncetoken。这个tokenstatic的,每个地方创建的是不一样的。见下面代码:

   dispatch_once_gate_t l = (dispatch_once_gate_t)val;

2.然后会对``l的底层原子性进行关联,关联到uintptr_t v的一个变量,通过os_atomic_load从底层取出,关联到变量v上。如果v这个值等于DLOCK_ONCE_DONE,也就是已经处理过一次了,就会直接返回。见下面代码:

    // 获取底层原子性的关联
   #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)) { // v  DLOCK_ONCE_DONE已经做过一次了,直接return

       return;
   }

如果之前没有执行过,原子处理比较其状态,进行解锁,最终会返回一个bool值,多线程情况下,只有一个能够获取锁返回yes。见下面代码:

//原子处理--对比,改变--解锁
static inline bool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{       //进行多线程锁处理,返回值是bool,如果是多线程只有一个线程能够获取锁返回yes
    return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
            (uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}

为保证了多线程安全性,通过_dispatch_lock_value_for_self上了一把锁,保证多线程安全。如果返回yes,就会执行_dispatch_once_callout方法,执行单例对应的任务,并对外广播,见下图:

_dispatch_once_callout

广播做了什么呢?进入_dispatch_once_gate_broadcast方法,见下图:

_dispatch_once_gate_broadcast

token通过原子比对,如果不是done,则设为done。同时对_dispatch_once_gate_tryenter方法中的锁进行处理。
_dispatch_once_mark_done

token标记为done之后,在入口处就会直接返回,见下图:

等待(_dispatch_once_wait)
如果存在多线程处理没有获取锁的情况,就会调用_dispatch_once_wait,进行等待,这里开启了自旋锁,内部进行原子处理,在loop过程中,如果发现已经被其他线程设置once_done了,则会进行放弃处理。见下图:

_dispatch_once_wait

总给

这篇文章就探索到这里咯,相信对GCD单例同步函数死锁异步函数的底层调度原理都有了深刻的理解。GCD的底层探索还没结束,下一篇文章我们探索GCD栅栏,信号量等相关功能的底层原理,不见不散。

你可能感兴趣的:(iOS GCD底层分析(2)--同步异步函数、死锁、GCD单例)