前言
之前,我们在探索动画及渲染相关原理的时候,我们输出了几篇文章,解答了
iOS动画是如何渲染,特效是如何工作的疑惑
。我们深感系统设计者在创作这些系统框架的时候,是如此脑洞大开,也深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。
因此我们决定
进一步探究iOS底层原理的任务
。继上一篇文章对GCD
的主队列
、串行队列
&&并行队列
、全局并发队列
探索之后,本篇文章将继续对GCD多线程底层原理的探索
一、 dispatch_get_global_queue全局并发队列+dispatch_sync同步函数
dq->dq_width == 1
为串行队列,那么并发队列该怎么走呢? 如下图,走的是下面的框框中流程 但是这么多的分支,到底是走的哪一个呢?通过对
_dispatch_sync_f_slow
、 _dispatch_sync_recurse
、_dispatch_introspection_sync_begin
、_dispatch_sync_invoke_and_complete
方法下符号断点,进行跟踪调试。
- 符号断点调试
通过下符号断点跟踪,发现走了
_dispatch_sync_f_slow
,如下图所示:
通过阅读源码,发现一个有意思的事情,就是
_dispatch_sync_invoke_and_complete
方法
_dispatch_sync_invoke_and_complete
在这个_dispatch_sync_invoke_and_complete
方法的第三个参数是func
也是需要执行的任务,但是 func
的后面的整体也是一个参数,也就是 DISPATCH_TRACE_ARG( _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags))
整体为一个参数,这就有意思了,中间居然没有逗号分隔开。老铁,你这很特别啊!够长的啊!
那么去DISPATCH_TRACE_ARG
定义看看 在
DISPATCH_TRACE_ARG
的宏定义里面,你们有没有发现,这里居然把逗号
放在了里面,好家伙,宏定义里面还可以这么玩,苹果工程师还真有意思哈! 通过全局的搜索,发现这个宏定义有两处,一个有逗号,一个没有逗号,这就是根据不同的条件,进行设置,相当于是一个
可选的参数
,这一波操作又是非常的细节了!
既然下符号断点会走_dispatch_sync_f_slow
方法,现在就去看看这个方法
_dispatch_sync_f_slow
这里又是很多的分支,又通过下符号断点,发现走的是
_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
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);
}
push
之后调用callout
执行,最后再pop
,所以可以同步的执行任务
二、 dispatch_async异步函数
dispatch_async
异步函数的任务,是包装在 qos
里面的,那么现在跟踪流程,去看看
dispatch_async
_dispatch_continuation_async
dx_push
搜索
dx_push
调用的地方 这里就先去看看并发队列里面的
dq_push
吧,
- _dispatch_lane_concurrent_push
这里
if
里面有对栅栏函数
(_dispatch_object_is_barrier)的判断,栅栏函数这里就不分析了,后续的博客里面会分析的。
在_dispatch_lane_concurrent_push
里面会去调用_dispatch_lane_push
方法,在上面搜索dx_push
的图里面,可以看到,在串行队列里面是直接调用了_dispatch_lane_push
,也就是说串行
和并发
都会走这个方法。
- _dispatch_lane_push
最后去调用
dx_wakeup
,再去搜索看看
dx_wakeup
是一个宏定义,看看dq_wakeup
哪里调用了 如上图可以发现,串行和并发都是
_dispatch_lane_wakeup
,全局的是_dispatch_root_queue_wakeup
- _dispatch_queue_wakeup
通过下符号断点会走_dispatch_lane_class_barrier_complete
_dispatch_lane_class_barrier_complete
里面循环递归一些操作,还看到了一个系统的函数os_atomic_rmw_loop2o
,在这个方法里面要么返回dx_wakeup
或者做其他的一些处理。
#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
通过跟流程和下符号断点,会走全局并发队列的_dispatch_root_queue_push
方法。通过下符号断点,跟踪源码,最终定位到一个重要的方法_dispatch_root_queue_poke_slow
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_slow
_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_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
,默认大小如下:
255
表示理论上线程池的最大数量。但是实际能开辟多少呢,这个不确定。在苹果官方完整Thread Management中,有相关的说明,辅助线程的最小允许堆栈大小为 16
KB,并且堆栈大小必须是4
KB 的倍数。见下图: 也就是说,一个辅助线程的栈空间是
512KB
,而一个线程所占用的最小空间是16KB
,也就是说栈空间一定的情况下,开辟线程所需的内存越大,所能开辟的线程数就越小。针对一个4GB
内存的iOS
真机来说,内存分为内核态和用户态,如果内核态全部用于创建线程,也就是1GB
的空间,也就是说最多能开辟1024KB / 16KB
个线程。当然这也只是一个理论值。
三、 单例
上面提到了单例,那么接下来就去分析一下单例 来看看简单的单例使用:
static dispatch_once_t token;
dispatch_once(&token, ^{
// 代码执行
});
- 单例的定义如下:
void
_dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block)
{
if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
dispatch_once(predicate, block);
} else {
dispatch_compiler_barrier();
}
DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l);
}
#undef dispatch_once
#define dispatch_once _dispatch_once
#endif
#endif // DISPATCH_ONCE_INLINE_FASTPATH
针对不同的情况作了一些特殊处理,比如栅栏函数
等,这里只分析dispatch_once
,进入dispatch_once
实现 单例是只会执行一次,那么这里就是利用
val
参数来进行控制的,接着去dispatch_once_f
里面看看 对
l
的底层原子性进行关联,关联到uintptr_t v
的一个变量,通过os_atomic_load
从底层取出,关联到变量v
上。如果v
这个值等于DLOCK_ONCE_DONE
,也就是已经处理过一次了,就会直接return
返回
_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_once_gate_tryenter
里面是进行原子操作,就是锁的处理,如果之前没有执行过,原子处理会比较它状态,进行解锁,最终会返回一个bool
值,多线程情况下,只有一个能够获取锁返回yes
。
if (_dispatch_once_gate_tryenter(l)) {
return _dispatch_once_callout(l, ctxt, func);
}
通过_dispatch_lock_value_for_self
上了一把锁,保证多线程安全。如果返回yes
,就会执行_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
执行任务_dispatch_once_gate_broadcast
对外广播,标记为done
_dispatch_once_gate_broadcast
广播
将
token
通过原子比对,如果不是done
,则设为done
。同时对_dispatch_once_gate_tryenter
方法中的锁进行处理。
_dispatch_once_mark_done
os_atomic_cmpxchg
是一个宏定义,先进行比较再改变,先比较 dgo
,在设置标记为DLOCK_ONCE_DONE
也就是 done
当token
标记为done
之后,就会直接返回,如存在多线程处理,没有获取锁的情况,就会调用_dispatch_once_wait
,如下下:
_dispatch_once_wait
,进行等待,这里开启了自旋锁
,内部进行原子处理,在loop
过程中,如果发现已经被其他线程设置once_done
了,则会进行放弃处理 那么任务的执行交给谁了呢?
通过打印堆栈信息,发现是交给了下层的线程,通过一些包装,给了底层的
pthread
这就可以说
GCD
底层是封装了pthread
,不管是 iOS
还是 Java
都是封装了底层的通用线程机制pthread
。
这里的执行是通过工作循环workloop
,工作循环的调起受 OS(受 CPU调度执行的。)管控的,异步线程的异步体现在哪里呢?就是体现在是否可以获得,而不是立即执行,而同步函数是直接调用执行的,而这里并没有看到异步的直接调用执行。
四、 sync 和 async 的区别
- 是否可以开启新的线程执行任务
- 任务的回调是否具有异步行、同步性
- 是否产生死锁问题
五、 死锁 源码分析
在前面篇幅的分析中,我们得知,同步 sync
函数的流程是:
_dispatch_sync_f
-- >_dispatch_sync_f_inline
-- >_dispatch_barrier_sync_f
走到
_dispatch_barrier_sync_f
流程中,这与上篇博客的分析是一致的,因为这里dq_width=1
,所以是串行队列
,如果是并发队列
,则会走到_dispatch_sync_f_slow
,现在去_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
方法
在这个方法里面,会对队列进行判断,是否存在等待或者挂起状态
//判断是否挂起、等待
if (unlikely(!_dispatch_queue_try_acquire_barrier_sync(dl, tid))){
// 添加任务
return _dispatch_sync_f_slow(dl, ctxt, func, DC_FLAG_BARRIER, dl,
DC_FLAG_BARRIER | dc_flags);
}
在之前的博客里面也提到了死锁
相关的内容,出现死锁会报和_dispatch_sync_f_slow
相关的错误,如下:
虽然死锁会走
_dispatch_sync_f_slow
方法,但是死锁的报错不是_dispatch_sync_f_slow
这个报错,而是如下图中所示的0
处报错了
真报错的是__DISPATCH_WAIT_FOR_QUEUE__
,那么现在去验证一下
_dispatch_sync_f_slow
在
_dispatch_sync_f_slow
方法内部,我们发现了刚刚死锁报错的__DISPATCH_WAIT_FOR_QUEUE__
,现在去内部看看
__DISPATCH_WAIT_FOR_QUEUE__
在__DISPATCH_WAIT_FOR_QUEUE__
内部,发现了和死锁报错信息基本一样,意思是:
dispatch_sync
在当前线程已经拥有的队列上调用 ,对不起兄弟,我已经拥有她了,你来晚一步了
if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
"dispatch_sync called on queue "
"already owned by current thread");
}
这个dsc_waiter
是由前面_dispatch_sync_f_slow
方法里面传过来来的
_dispatch_tid_self()
是线程id
,定义如下
_dispatch_thread_port
是线程的通道,现在再去看看线程状态的匹配
//状态
uint64_t dq_state = _dispatch_wait_prepare(dq);
if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
"dispatch_sync called on queue "
"already owned by current thread");
}
_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
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;
}
DLOCK_OWNER_MASK
#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc)
这里就是死锁的判断:异或
再作与
操作,也就是结果为0
就是死锁。翻译一下就是dq_state ^ dsc->dsc_waiter
的结果为 0
再和DLOCK_OWNER_MASK
作与
操作等于0
。
那么dq_state ^ dsc->dsc_waiter
的结果什么情况下会为 0
呢?异或是相同为0
,因为DLOCK_OWNER_MASK
是一个非常大的整数,所以dq_state
和 dsc->dsc_waiter
都是为0
。
当前队列里面要等待的线程 id
和我调用的是一样,我已经处于等待状态
,你现在有新的任务过来需要使用我去执行,这样产生了矛盾,进入相互等待
状态,进而产生死锁
。这就是串行队列执行同步任务产生死锁的原因!
专题系列文章
1.前知识
- 01-探究iOS底层原理|综述
- 02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探究iOS底层原理|LLDB
- 04-探究iOS底层原理|ARM64汇编
2. 基于OC语言探索iOS底层原理
- 05-探究iOS底层原理|OC的本质
- 06-探究iOS底层原理|OC对象的本质
- 07-探究iOS底层原理|几种OC对象【实例对象、类对象、元类】、对象的isa指针、superclass、对象的方法调用、Class的底层本质
- 08-探究iOS底层原理|Category底层结构、App启动时Class与Category装载过程、load 和 initialize 执行、关联对象
- 09-探究iOS底层原理|KVO
- 10-探究iOS底层原理|KVC
- 11-探究iOS底层原理|探索Block的本质|【Block的数据类型(本质)与内存布局、变量捕获、Block的种类、内存管理、Block的修饰符、循环引用】
- 12-探究iOS底层原理|Runtime1【isa详解、class的结构、方法缓存cache_t】
- 13-探究iOS底层原理|Runtime2【消息处理(发送、转发)&&动态方法解析、super的本质】
- 14-探究iOS底层原理|Runtime3【Runtime的相关应用】
- 15-探究iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探究iOS底层原理|RunLoop的应用
- 17-探究iOS底层原理|多线程技术的底层原理【GCD源码分析1:主队列、串行队列&&并行队列、全局并发队列】
- 18-探究iOS底层原理|多线程技术【GCD源码分析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探究iOS底层原理|多线程技术【GCD源码分析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探究iOS底层原理|多线程技术【GCD源码分析3:线程调度组dispatch_group、事件源dispatch Source】
- 21-探究iOS底层原理|多线程技术【线程锁:自旋锁、互斥锁、递归锁】
- 22-探究iOS底层原理|多线程技术【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探究iOS底层原理|内存管理【Mach-O文件、Tagged Pointer、对象的内存管理、copy、引用计数、weak指针、autorelease
3. 基于Swift语言探索iOS底层原理
关于函数
、枚举
、可选项
、结构体
、类
、闭包
、属性
、方法
、swift多态原理
、String
、Array
、Dictionary
、引用计数
、MetaData
等Swift基本语法和相关的底层原理文章有如下几篇:
其它底层原理专题
1.底层原理相关专题
2.iOS相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决方案