这是我参与8月更文挑战的第14天,活动详情查看: 8月更文挑战
在之前的文章中,我们介绍了函数和队列,也了解了函数的堆栈的调用;今天我们来分析一下同步函数的死锁是怎么发生的?
同步函数死锁
我们从同步函数的调用入口dispatch_sync为切入点来进行分析:
dispatch_sync
unlikely意为不大可能发生的,通常认为其后的条件为0;在此处的意思是_dispatch_block_has_private_data(work)函数返回结果通常是false;关于这个dc_flags官方的解释是:如果dc_flags小于0x1000,则该对象是延续。否则,对象有一个私有的布局和内存管理规则;- 所以我们继续分析的应该是
_dispatch_sync_f方法,dq是我们的函数所处的队列,在此处针对work进行了一层_dispatch_Block_invoke的封装;work就是我们同步函数执行的回调,也就是我们具体的任务; DC_FLAG_BLOCK是一个宏定义,其值为0x010ul;- 我们分析过程中的重点应该放在
dq和work上;
_dispatch_sync_f
dq为我们的函数所处的队列;ctxt指向我们的work,也就是具体执行的任务;func指的是_dispatch_Block_invoke(work);dc_flags值为0x010ul
_dispatch_sync_f_inline
likely意为:经常发生,很可能发生的dq_width == 1,在上一篇文章中我们已经分析过了,dq_width == 1表示当前队列是一个串行队列;_dispatch_barrier_sync_f产生死锁;
_dispatch_barrier_sync_f
继续调用:_dispatch_barrier_sync_f_inline
_dispatch_barrier_sync_f_inline
_dispatch_sync_f_slow这个函数我们应该有印象,在上一篇文章中,我们演示死锁时,发生异常时的堆栈信息最终调用的就是_dispatch_sync_f_slow;
但是需要注意的是,_dispatch_sync_f_slow的调用并没有报错,最终报错的是__DISPATCH_WAIT_FOR_QUEUE__:
-
真机
-
模拟器
_dispatch_sync_f_slow
__DISPATCH_WAIT_FOR_QUEUE__就是最终导致死锁报错的地方;
DISPATCH_WAIT_FOR_QUEUE
我们发现在此函数中出现了与模拟器一摸一样的死锁日志信息:
那么,进一步说明了模拟器的死锁出现在了此处,接下来我们来分析一下死锁的条件;
- 首先根据日志信息:同步函数调用的队列也就是同步函数所在的队列,已经被当前的线程持有了;
_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter):根据_dispatch_sync_f_slow函数实现我们知道dsc->dsc_waiter指向了_dispatch_tid_self(),_dispatch_tid_self()就是当前队列线程的线程ID:
#if TARGET_OS_MAC
#define _dispatch_tid_self() ((dispatch_tid)_dispatch_thread_port())
#elif defined(__linux__)
#define _dispatch_tid_self() ((dispatch_tid)(_dispatch_get_tsd_base()->tid))
#elif defined(_WIN32)
#define _dispatch_tid_self() ((dispatch_tid)(_dispatch_get_tsd_base()->tid << 2))
dq_state:当前等待的队列的状态
_dq_state_drain_locked_by
_dispatch_lock_is_locked_by
DLOCK_OWNER_MASK值为#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc),是一个很大的值((lock_value ^ tid) & DLOCK_OWNER_MASK)只要(lock_value ^ tid)不为0,那么结果就一定不为0,那么此处很明显(lock_value ^ tid)结果为0;(lock_value ^ tid)结果为0说明lock_value与tid相同;- 也就是当前要调用的队列
dq目前是等待状态,所以发生了死锁;
总结
如果当前将要调用的队列处于等待状态,那么就会产生
死锁; 需要注意的是,串行队列同步函数并不是一定会导致死锁;