iOS底层学习——GCD底层原理分析(同步异步函数、死锁、GCD单例)

在上篇GCD函数和队列原理探索中,我们遗留了一些问题,如:

  • 死锁是如何产生的?
  • 对于异步函数,线程在哪里开辟?
  • 底层通过_dispatch_worker_thread2方法完成任务的回调执行,那么触发调用的位置在哪?
  • GCD中单例的逻辑是怎样的?

本篇文章,将这些内容补充完整。

  • 引入一个问题,同步函数和异步函数的区别?

    1. 是否开辟线程
    2. 函数的调用的异步性和同步性
    3. 死锁情况的产生

下面详细分析

1.同步函数

在上一篇文章中,分析同步函数时,已经跟踪到了_dispatch_sync_f_inline流程,见下图:

image.png

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

具体同步函数分析过程见GCD函数和队列原理探索。这里不再阐述,这里我们重点分析一下同步函数串行队列的死锁情况。

  • 死锁

    进入_dispatch_barrier_sync_f流程,分析同步串行执行流程。见下图:

    image.png

    该方法会调用_dispatch_barrier_sync_f_inline,这个方法中有一个判断,判断当前队列是否存在等待或者挂起状态,见下图:

    image.png

    查看该判断方法,见下图:

    image.png

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

    那么_dispatch_sync_f_slow这个方法中哪一行代码才是真正的死锁的反馈呢?

    在分析之前,我们先造一个死锁的代码,看看其哪里报错。见下图代码:

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

    上面的案例会导致死锁。在当前的主线程,有一个同步函数,并向主队列中添加任务。运行程序,见下图:

    image.png

    运行结构和我们的分析是一致的,死锁最终调用了_dispatch_sync_f_slow方法,而真正导致死锁的位置是__DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq);,见下图:

    image.png

    进入__DISPATCH_WAIT_FOR_QUEUE__方法,查看其实现,见下图:

    image.png

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

    image.png

    其中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。(具体异步函数分析过程见GCD函数和队列原理探索这里不再阐述)源码如下:

image.png

前面也分析了_dispatch_root_queues_init方法,使用了单例。在该方法中,采用单例的方式进行了线程池的初始化处理工作队列的配置工作队列的初始化等工作。同时这里有一个关键的设置,执行函数的设置,也就是将任务执行的函数被统一设置成了_dispatch_worker_thread2。见下图:

image.png

这里的调用执行是通过workloop工作循环调用起来的,也就是说并不是及时调用的,而是通过os完成调用,说明异步调用的关键是在需要执行的时候能够获取对应的方法,进行异步处理,而同步函数是直接调用

那么它调用的位置在哪呢?继续分析_dispatch_root_queue_poke_slow方法。如果是全局队列,此时会创建线程进行执行任务,见下图:

image.png

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

image.png

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

image.png

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

  • 能够开辟多少线程呢?

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

    image.png

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

    image.png

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

    image.png

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

3.GCD单例

  • 单例使用

    只能执行一次。

       static dispatch_once_t token;
    
       dispatch_once(&token, ^{
           // code
       });
    复制代码
  • 单例的定义

    要想分析其实现原理,首先我们要找到GCD单例的定义。见下图:

    image.png

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

    image.png

    这里针对不同的情况作了一些特殊处理,比如栅栏函数等,这里只分析dispatch_once,进入dispatch_once实现,见下图:

    image.png

    最终会调用dispatch_once_f,源码实现见下图:

    image.png

  • 实现逻辑

    思考:如果我们要控制一段代码只执行一次,应该怎么处理呢?首先要创建一个标识,如果标识已经被特殊标记,说明已经执行过了;如果没有被特殊标记过,说明可以进行执行。同时为了保证线程安全,在关键流程中需要加锁。

    下面来分析源码实现逻辑。

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

       dispatch_once_gate_t l = (dispatch_once_gate_t)val;
    复制代码

    紧接着会对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。见下面代码:

    image.png

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

    image.png

    广播做了什么呢?见下图:

    image.png

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

    image.png

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

    image.png

  • 等待

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

    image.png

猜你喜欢

转载自juejin.im/post/6994756674573565989