iOS底层学习——GCD底层原理分析(同步异步函数、死锁、GCD单例)
-
死锁是如何产生的?
-
对于异步函数,线程在哪里开辟?
-
底层通过
_dispatch_worker_thread2
方法完成任务的回调执行,那么触发调用的位置在哪? -
GCD
中单例的逻辑是怎样的? 本篇文章,将这些内容补充完整。 -
引入一个问题,同步函数和异步函数的区别?
- 是否开辟线程
- 函数的调用的异步性和同步性
- 死锁情况的产生
下面详细分析
1.同步函数
在上一篇文章中,分析同步函数时,已经跟踪到了_dispatch_sync_f_inline
流程,见下图:
通过符号断点,我们可以确定如果队列为串行队列,会走到_dispatch_barrier_sync_f
流程中,这与我们的分析也是一致的,因为这里dq_width=1
,所以是串行队列。如果是并发队列,则会走到_dispatch_sync_f_slow
。
-
死锁
进入
_dispatch_barrier_sync_f
流程,分析同步串行执行流程。见下图:该方法会调用
_dispatch_barrier_sync_f_inline
,这个方法中有一个判断,判断当前队列是否存在等待或者挂起状态,见下图:查看该判断方法,见下图:
在该流程中会对队列的状态进行判断,放弃底层的执行流程,也就是让队列不再调度别的任务,返回控制处理。如果当前队列处于挂起或者阻塞状态会执行
_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__
方法,查看其实现,见下图:首先会获取当前要使用队列的状态,然后调用
_dq_state_drain_locked_by
方法和当前的队列进行比较,满足一定条件即视为死锁
。进入_dq_state_drain_locked_by
方法,查看其判断逻辑,见下图:其中
DLOCK_OWNER_MASK
是一个很大的数,说明当lock_value ^ tid = 0
时,才会返回0
,也就是说此时使用的队列和当前等待的队列是同一个队列。#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc)
至此死锁的逻辑就清楚了,
当前队列处于等待状态,而又有新的任务过来需要使用这个队列去调度,这样产生了矛盾,进入相互等待状态,进而产生死锁
。
2.异步函数
前面也分析了_dispatch_root_queues_init
方法,使用了单例。在该方法中,采用单例的方式进行了线程池的初始化处理
、工作队列的配置
、工作队列的初始化
等工作。同时这里有一个关键的设置,执行函数的设置,也就是将任务执行的函数被统一设置成了_dispatch_worker_thread2
。见下图:
这里的调用执行是通过workloop
工作循环调用起来的,也就是说并不是及时调用的,而是通过os
完成调用,说明异步调用的关键是在需要执行的时候能够获取对应的方法,进行异步处理,而同步函数是直接调用
。
那么它调用的位置在哪呢?继续分析_dispatch_root_queue_poke_slow
方法。如果是全局队列,此时会创建线程进行执行任务,见下图:
对线程池进行处理,从线程池中获取线程,执行任务,同时判断线程池的变化。见下图:
remaining
可以理解为当前可用线程数,当可用线程数等于0
时,线程池已满pthread pool is full
,直接return
。底层通过pthread
完成线程的开辟,见下图:
也就是_dispatch_worker_thread2
是通过pthread
完成oc_atmoic
原子触发。
-
能够开辟多少线程呢?
通过解读前面的源码,发现队列线程池的大小为:
dgq_thread_pool_size
。dgq_thread_pool_size
被赋值为:thread_pool_size
,见下图:thread_pool_size
的初始值为:DISPATCH_WORKQ_MAX_PTHREAD_COUNT
。全局搜索,定义如下:255
表示理论上线程池的最大数量。但是实际能开辟多少呢,这个不确定。在苹果官方完整Thread Management中,有相关的说明,辅助线程的最小允许堆栈大小为16 KB
,并且堆栈大小必须是4 KB
的倍数。见下图:也就是说,一个辅助线程的栈空间是
512KB
,而一个线程所占用的最小空间是16KB
,也就是说栈空间一定的情况下,开辟线程所需的内存越大,所能开辟的线程数就越小。针对一个4GB
内存的iOS
真机来说,内存分为内核态和用户态,如果内核态全部用于创建线程,也就是1GB
的空间,也就是说最多能开辟1024KB / 16KB
个线程。当然这也只是一个理论值。
3.GCD单例
-
单例使用
只能执行一次。
static dispatch_once_t token; dispatch_once(&token, ^{ // code });
-
单例的定义
要想分析其实现原理,首先我们要找到
GCD
单例的定义。见下图:在
libdispatch.dylib
源码中全局搜索_dispatch_once
,见下图:这里针对不同的情况作了一些特殊处理,比如栅栏函数等,这里只分析
dispatch_once
,进入dispatch_once
实现,见下图:最终会调用
dispatch_once_f
,源码实现见下图: -
实现逻辑
思考:如果我们要控制一段代码只执行一次,应该怎么处理呢?首先要创建一个标识,如果标识已经被特殊标记,说明已经执行过了;如果没有被特殊标记过,说明可以进行执行。同时为了保证线程安全,在关键流程中需要加锁。
下面来分析源码实现逻辑。
首先会对传入的
val
进行数据包装,包装成l
,这个val
就是外面创建的oncetoken
。这个token
是static
的,每个地方创建的是不一样的。见下面代码: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
。见下面代码:为保证了多线程安全性,通过
_dispatch_lock_value_for_self
上了一把锁,保证多线程安全。如果返回yes
,就会执行_dispatch_once_callout
方法,执行单例对应的任务,并对外广播,见下图:广播做了什么呢?见下图:
将
token
通过原子比对,如果不是done
,则设为done
。同时对_dispatch_once_gate_tryenter
方法中的锁进行处理。当
token
标记为done
之后,在入口处就会直接返回,见下图: -
等待
如果存在多线程处理,没有获取锁的情况,就会调用
_dispatch_once_wait
,进行等待,这里开启了自旋锁,内部进行原子处理,在loop
过程中,如果发现已经被其他线程设置once_done
了,则会进行放弃处理。见下图:
转载自:https://juejin.cn/post/6994756674573565989