前言
上一篇文章iOS GCD底层分析(1)留下了四个问题,分别是:
- 死锁底层是怎么样子产生的?
- 如果是异步函数,线程是怎样子创建的?
- 底层通过
_dispatch_worker_thread2
方法完成任务的回调执行,那么触发调用的位置在哪? - 单例的底层原理是什么?
准备工作
- libdispatch.dylib
- iOS GCD底层分析(1)
1. 同步函数
上一篇文章中分系同步函数时进入了_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_inline
方法,发现起内部有一个判断,是用来判断当前队列是否等待或者挂起
,如下:
进入
_dispatch_queue_try_acquire_barrier_sync
判断方法,如下:
在该流程中会对
队列的状态进行判断
,放弃底层的执行流程
,也就是让队列不再调度别的任务
,返回控制处理。如果当前队列处于挂起
或者阻塞
状态会执行_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. 异步函数
上一篇文章中,我们通过dx_push
定位到,底层为不同类型的队列提供不同的调用入口,比如全局并发队列
会调用_dispatch_root_queue_push
方法。通过下符号断点,跟踪源码,最终定位到一个重要的方法_dispatch_root_queue_poke_slow
,进入此方法如下:
前面也分析了
_dispatch_root_queues_init
方法,使用了单例
。在该方法中,采用单例
的方式进行了线程池的初始化处理
、工作队列的配置
、工作队列的初始化
等工作。同时这里有一个关键的设置,执行函数的设置
,也就是将任务执行的函数被统一设置成了_dispatch_worker_thread2
。见下图:
这里的调用执行是通过
workloop
工作循环调用起来的,也就是说并不是及时调用的,而是通过os
完成调用,说明异步调用的关键是在需要执行的时候能够获取对应的方法,进行异步处理,而同步函数是直接调用
。
疑问:_dispatch_worker_thread2
在哪里调用呢?继续深入分析_dispatch_root_queue_poke_slow
方法
由上图可知,如果是
全局队列
,此时会创建线程进行执行任务
。
对线程池
进行处理,从线程池中获取线程,执行任务,同时判断线程池的变化。见下图:
remaining
可以理解为当前可用线程数
,当可用线程数等于0
时,线程池已满pthread pool is full
,直接return
。底层通过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_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
的空间,也就是说最多能开辟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_f
,进入此方法查看源码实现见下图:
初步流程分析:
首先要
创建一个标识
,如果标识已经被特殊标记,说明已经执行过了;如果没有被特殊标记过,说明可以进行执行。同时为了保证线程安全
,在关键流程中需要加锁
。
整个创建流程分析:
1.首先会对传入的val
进行数据包装,包装成l
,这个val
就是外面创建的oncetoken
。这个token
是static
的,每个地方创建的是不一样
的。见下面代码:
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_gate_broadcast
方法,见下图:
将
token
通过原子比对,如果不是done
,则设为done
。同时对_dispatch_once_gate_tryenter
方法中的锁进行处理。
当
token
标记为done
之后,在入口处就会直接返回,见下图:
等待(_dispatch_once_wait)
如果存在多线程处理
,没有获取锁
的情况,就会调用_dispatch_once_wait
,进行等待
,这里开启了自旋锁
,内部进行原子处理
,在loop
过程中,如果发现已经被其他线程设置once_done
了,则会进行放弃
处理。见下图:
总给
这篇文章就探索到这里咯,相信对GCD单例
,同步函数死锁
,异步函数的底层调度
原理都有了深刻的理解。GCD
的底层探索还没结束,下一篇文章我们探索GCD
栅栏,信号量等相关功能的底层原理,不见不散。