GCD死锁
我们知道,当在主线程执行如下代码时,会出现死锁。
dispatch_sync(dispatch_get_main_queue(), ^{
});
复制代码
那出现死锁的原因又到底是因为什么呢?
首先运行这段代码,当出现崩溃时,查看堆栈。
我们可以发现,崩溃的时候是在__DISPATCH_WAIT_FOR_QUEUE__
这一个地方,那__DISPATCH_WAIT_FOR_QUEUE__
又是什么?接下来尝试在GCD的源码中搜索。
static void __DISPATCH_WAIT_FOR_QUEUE__(dispatch_sync_context_t dsc, dispatch_queue_t dq)
{
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");
}
// 省略后面的代码...
}
复制代码
在源码中找到了一个__DISPATCH_WAIT_FOR_QUEUE__
函数,其中有两个字符串,"dispatch_sync called on queue"
和"already owned by current thread"
,大致的意思就是调用dispatch_sync
函数的queue,已经是当前线程的queue。那具体是不是这样子的呢?就先查看一下if判断语句里的_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);
}
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;
}
#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc)
复制代码
判断语句最终的判断代码就是return ((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0;
。DLOCK_OWNER_MASK
又是一个很大的值,如果外层括号里的运算要==0,内层的括号里两个值异或后的结果必须是0。
那lock_value
和tid
又代表着什么呢?
参数传递进来的是(dispatch_lock)dq_state
,查看dispatch_lock
,实际上是一个uint32_t
类型,tid
也是一样。再往上面一层调用__DISPATCH_WAIT_FOR_QUEUE__
中去查看,第一个参数是由_dispatch_wait_prepare
函数构造出来的,那就先看一下这个函数的源码。
static inline uint64_t
_dispatch_wait_prepare(dispatch_queue_t dq)
{
uint64_t old_state, new_state;
os_atomic_rmw_loop2o(dq, dq_state, old_state, new_state, relaxed, {
if (_dq_state_is_suspended(old_state) ||
!_dq_state_is_base_wlh(old_state) ||
!_dq_state_in_uncontended_sync(old_state)) {
os_atomic_rmw_loop_give_up(return old_state);
}
new_state = old_state | DISPATCH_QUEUE_ RECEIVED_SYNC_WAIT;
});
return new_state;
}
复制代码
该函数包含一些old_state
、new_state
的变量,并最终返回,并且有一行较为关键的代码new_state = old_state | DISPATCH_QUEUE_RECEIVED_SYNC_WAIT;
,可以根据代码和宏定义的名称推测出,当前面if语句条件不满足时,new_state
将会是一个等待的状态,并且返回了这个状态。由此推测,lock_value
就是dispatch_queue_t dq
当前队列是否等待的一个状态。同时根据dsc->dsc_waiter
推测,第二个参数应该也是dispatch_sync_context_t
的等待状态,而这个名称代表着上下文,应该就是当前的线程。
那么回到((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0
中,要想整个等式最后能等于0,那么lock_value
和tid
必须是相等的,根据刚才的分析,这两个值是当前队列的等待状态和当前线程的等待状态,所以当二者都是等待状态的时候,发生死锁。这与众所周知的死锁原因是一致的。
最后用一句话来总结一下:在同个线程里,同步向串行队列里面添加任务,造成任务相互等待,就会死锁。
死锁分析案例
案例1
dispatch_queue_t t = dispatch_queue_create("qt", DISPATCH_QUEUE_SERIAL);
dispatch_sync(t, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
});
});
复制代码
最终结果是发生死锁。首先t是一个串行队列,然后调用同步函数,此时是没有开辟新线程的,依然是在主线程上,然后在主线程中继续使用同步函数dispatch_sync
去获取主线程的队列,这跟直接调用同步函数并使用主队列是一样的,就会造成死锁。
案例2
dispatch_queue_t t = dispatch_queue_create("qt", DISPATCH_QUEUE_SERIAL);
dispatch_sync(t, ^{
dispatch_sync(t, ^{
});
});
复制代码
这个案例与第一个案例的区别就是在调用第二个同步函数时,使用的的是队列t,所以依然是在主线程中对串行队列t中又给队列t添加任务,所以还是会造成死锁。
案例3
dispatch_queue_t t = dispatch_queue_create("qt", DISPATCH_QUEUE_SERIAL);
dispatch_async(t, ^{
dispatch_sync(t, ^{
});
});
复制代码
这个案例根据案例2将第一个函数改成了异步队列,异步函数会开辟线程,但是在子线程中,依然是在t队列中又同步地给t队列添加任务,所以依然会造成死锁。
案例4
1. dispatch_queue_t t = dispatch_queue_create("qt", DISPATCH_QUEUE_SERIAL);
2. dispatch_queue_t t2 = dispatch_queue_create("qt2", DISPATCH_QUEUE_SERIAL);
3. dispatch_sync(t, ^{
4. dispatch_async(t2, ^{
5. dispatch_sync(t, ^{
6. NSLog(@"task2");
7. });
8. });
9. sleep(5);
10. NSLog(@"task1");
11. });
复制代码
在这个案例中,第4行使用了异步函数并且是向t2
队列添加任务,在任务中又向t
队列添加了一个任务。在队列t
中,先是在第3行添加了任务task1,然后又在第5行中添加了任务task2。但由于第4行使用异步函数开辟了新线程,所以task1无需等待task2完成任务即可执行,但在t
中,task2在task1后面,所以task2必须等taks1执行完毕之后才能执行,所以不论sleep了多久,打印的task1
都会在task2
之前。
根据案例,我们还可以总结一个规律:在同个线程中,执行串行队列的任务t1,如果t1依赖于t2的执行,而t2又是在该串行队列中t1之后的任务,就可以判断会产生死锁。
同步函数
在了解完死锁以后,我们来探索一下同步函数。
void
dispatch_sync(dispatch_queue_t dq, dispatch_block_t work)
{
uintptr_t dc_flags = DC_FLAG_BLOCK;
if (unlikely(_dispatch_block_has_private_data(work))) {
return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
}
_dispatch_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
}
#define _dispatch_Block_invoke(bb) \
((dispatch_function_t)((struct Block_layout *)bb)->invoke)
复制代码
同步函数有两个参数,第一个参数dq
就是任务队列,第二个参数work
就是实际执行的代码。在同步函数的源码中,又调用了_dispatch_sync_f
函数,其中第三个参数中,使用了宏将执行的代码封装成了Block_layout
类型的结构体,并且最后调用->invoke
去执行,所以这个就是实际执行添加的代码的地方。那么接着查看_dispatch_sync_f
函数的实现。
static void
_dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func, uintptr_t dc_flags)
{
_dispatch_sync_f_inline(dq, ctxt, func, dc_flags);
}
static inline void _dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt,
dispatch_function_t func, uintptr_t dc_flags)
{
if (likely(dq->dq_width == 1)) {
return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags);
}
if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
}
dispatch_lane_t dl = upcast(dq)._dl;
// Global concurrent queues and queues bound to non-dispatch threads
// always fall into the slow case, see DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
if (unlikely(!_dispatch_queue_try_reserve_sync_width(dl))) {
return _dispatch_sync_f_slow(dl, ctxt, func, 0, dl, dc_flags);
}
if (unlikely(dq->do_targetq->do_targetq)) {
return _dispatch_sync_recurse(dl, ctxt, func, dc_flags);
}
_dispatch_introspection_sync_begin(dl);
_dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG(
_dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags)));
}
复制代码
最终调用的是_dispatch_sync_f_inline
函数,并且将实际执行block代码以第三个参数func
传入,那我们就重点看看在哪里有使用了func
。
查看源码发现,使用到func
的地方有多处,光看源码并不能知道代码执行的流程,那么接下来就通过符号断点的方式来探索一下。
首先使用一个全局队列,并打上断点。 接下来将_dispatch_sync_f
函数中所有使用到func
的函数都打上符号断点后继续执行,发现执行了_dispatch_sync_f_slow
函数。 那就接着查看_dispatch_sync_f_slow
函数的实现。
static void
_dispatch_sync_f_slow(dispatch_queue_class_t top_dqu, void *ctxt,
dispatch_function_t func, uintptr_t top_dc_flags,
dispatch_queue_class_t dqu, uintptr_t dc_flags)
{
dispatch_queue_t top_dq = top_dqu._dq;
dispatch_queue_t dq = dqu._dq;
if (unlikely(!dq->do_targetq)) {
return _dispatch_sync_function_invoke(dq, ctxt, func);
}
// 省略部分代码...
_dispatch_sync_invoke_and_complete_recurse(top_dq, ctxt, func,top_dc_flags
DISPATCH_TRACE_ARG(&dsc));
}
复制代码
继续重复刚才的步骤,把func
相关函数打上符号断点后执行,调用了_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);
}
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);
}
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
_dispatch_get_tsd_base();
void *u = _dispatch_get_unwind_tsd();
if (likely(!u)) return f(ctxt);
_dispatch_set_unwind_tsd(NULL);
f(ctxt);
_dispatch_free_unwind_tsd();
_dispatch_set_unwind_tsd(u);
}
复制代码
由源码我们发现,最终在_dispatch_client_callout
中执行了任务代码f(ctxt);
。
接下来我们尝试执行自己创建的队列。
dispatch_queue_t t = dispatch_queue_create("qt", DISPATCH_QUEUE_SERIAL);
dispatch_sync(t, ^{
});
复制代码
同样,我们使用符号断点来进行调试,虽然执行的代码过程不完全一样,但是我们发现最终也是执行到_dispatch_client_callout
中的f(ctxt);
。
在探索的过程中,我们在源码中并没有发现线程的相关的代码,也说明了同步函数没有开辟线程,同时,同步函数执行的过程中没有对需要执行的任务进行保存或拷贝,只是一层一层往下传递,最终在_dispatch_client_callout
中执行,所以调用同步函数的时候,必须得等同步函数的任务执行完成以后,才能继续执行其他的代码。
那么我们就可以总结出同步函数的特点。
- 立即执行
- 阻塞当前线程
- 不具备开辟子线程的能力
异步函数
看完同步函数之后,我们再来看看异步函数。首先先看看源码。
void
dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
{
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME;
dispatch_qos_t qos;
qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
复制代码
根据刚才的经验,我们把重点放在参数work
上,所以接下来查看_dispatch_continuation_init
函数的实现。
static inline dispatch_qos_t
_dispatch_continuation_init(dispatch_continuation_t dc,
dispatch_queue_class_t dqu, dispatch_block_t work,
dispatch_block_flags_t flags, uintptr_t dc_flags)
{
void *ctxt = _dispatch_Block_copy(work);
dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
if (unlikely(_dispatch_block_has_private_data(work))) {
dc->dc_flags = dc_flags;
dc->dc_ctxt = ctxt;
// will initialize all fields but requires dc_flags & dc_ctxt to be set
return _dispatch_continuation_init_slow(dc, dqu, flags);
}
dispatch_function_t func = _dispatch_Block_invoke(work);
if (dc_flags & DC_FLAG_CONSUME) {
func = _dispatch_call_block_and_release;
}
return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}
复制代码
在_dispatch_continuation_init
函数中的第一行对work
进行了copy操作,并存储在ctxt
变量中,同时跟同步函数一样,也将调用使用_dispatch_Block_invoke
宏来将work
的实际调用封装成dispatch_function_t
类型的对象func
,并作为参数调用_dispatch_continuation_init_f
函数,接下来就查看_dispatch_continuation_init_f
函数的实现。
static inline dispatch_qos_t
_dispatch_continuation_init_f(dispatch_continuation_t dc,
dispatch_queue_class_t dqu, void *ctxt, dispatch_function_t f,
dispatch_block_flags_t flags, uintptr_t dc_flags)
{
pthread_priority_t pp = 0;
dc->dc_flags = dc_flags | DC_FLAG_ALLOCATED;
dc->dc_func = f;
dc->dc_ctxt = ctxt;
// in this context DISPATCH_BLOCK_HAS_PRIORITY means that the priority
// should not be propagated, only taken from the handler if it has one
if (!(flags & DISPATCH_BLOCK_HAS_PRIORITY)) {
pp = _dispatch_priority_propagate();
}
_dispatch_continuation_voucher_set(dc, flags);
return _dispatch_continuation_priority_set(dc, dqu, pp, flags);
}
复制代码
在_dispatch_continuation_init_f
中,把work
和f
都封装到了dispatch_continuation_t
类型的对象dc
中,最后调用了_dispatch_continuation_priority_set
,而_dispatch_continuation_priority_set
最后也就是返回了一个dispatch_qos_t
类型的对象,似乎并没有真正执行任务。那我们只能回到dispatch_async
函数中接着探索。
在通过_dispatch_continuation_init
函数返回一个qos
对象后,又将qos
作为参数调用了_dispatch_continuation_async
函数,那就接着查看这个函数,看看能否有什么发现。
_dispatch_continuation_async(dispatch_queue_class_t dqu,
dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
{
#if DISPATCH_INTROSPECTION
if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
_dispatch_trace_item_push(dqu, dc);
}
#else
(void)dc_flags;
#endif
return dx_push(dqu._dq, dc, qos);
}
复制代码
在_dispatch_continuation_async
中,随后return时使用了dx_push
宏并将qos
作为参数,接着查看这个宏。
#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
复制代码
再往后看并没有发现一些有用的代码,只看源码我们并不能知道流程的来龙去脉,那就换个思路。
我们写一个异步函数的调用,然后打上断点,再查看堆栈信息。
通过断点我们发现了,当使用异步函数的时候,代码块并不是在主线程(Thread1)中执行的,而是在Thread5执行的,我们打印一下当前线程,线程的number=6(number从1开始,主线程number=1)。并且在堆栈信息中,我们发现最终异步函数也是调用了_dispatch_client_callout
函数,这与同步函数最终的执行也是一样的,只不过同步函数是在主线程执行的。
那么我们也可以总结出异步函数的特点
- 可以开辟子线程,在子线程中执行任务
- 不会立即执行
- 不会阻塞当前线程
面试题
案例1
-(void)test1 {
dispatch_queue_t queue = dispatch_queue_create("dqc", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
}
复制代码
本案例中,queue是并发队列,所以不会产生死锁。1,2使用异步函数,所以1,2的调用顺序不确定。3使用同步函数本质也是在主线程,并且会阻塞线程,0在3之后在主线程中执行,所以0一定在3后面,但是3或0和1,2并没有绝对的顺序,1,2可能在3或0之前执行,也可能在3或0之后执行。7,8,9虽然开辟线程,但是代码自上而下执行,所以7,8,9一定是在0之后执行,它们也没有绝对的执行顺序。
案例2
-(void)test2 {
dispatch_queue_t t = dispatch_queue_create("lg", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_sync(t, ^{
NSLog(@"2");
dispatch_async(t, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
复制代码
本案例1肯定先执行,由于是同步函数,2一定在1之后执行,3是在异步函数中,所以3执行的顺序在2之后,与4,5的顺序就不一定了。4还是在同步函数中,所以一定会在2之后执行,但与3的顺序不一定。5是在同步函数之后才调用的,所以会等同步函数中的2,4执行完毕之后才会执行,但是3是在异步函数中,所以5和3的执行顺序也不一定。
总结下来就是,1,2,4,5这个顺序是一定的,3在4,5的哪个顺序是不一定的。
案例3
-(void)test3 {
dispatch_queue_t t = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_sync(t, ^{
NSLog(@"2");
dispatch_async(t, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
复制代码
t是串行队列,虽然调用的是同步函数往t中加任务,但是当前队列是主队列,在主队列中使用同步函数往t中加任务不会死锁。1,2的顺序是肯定的,3是开辟了子线程,但依然是往队列t中添加的任务,至于什么时候添加到t中,并不能知道。由于3是开辟了线程,并不会阻塞当前线程,而且开辟线程需要耗时,3并不知道什么时候能执行,所以4会比3先添加到队列t中,队列又是先进先出的,所以4一定会比3更早执行。5和4也是同理,在执行完4之后把5添加队列t中,此时不知道3是否被添加到队列中所以5和3的顺序是不一定的,但5一定在4后面。
案例4
-(void)test4 {
dispatch_queue_t t = dispatch_queue_create("lg", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(t, ^{
NSLog(@"2");
dispatch_sync(t, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
复制代码
t是并行队列,2是在异步函数中,所以5先执行,然后执行2,3又是在同步函数中,所以2执行完以后执行3,最后执行4。
案例5
- (void)test5 {
self.num = 0;
while (self.num < 100) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.num ++;
});
}
NSLog(@"self.num = %d",self.num);
}
复制代码
num至少要是100才能结束while循环,由于num++的操作是异步的,所以当num=100之后,可能还有线程继续对num进行++的操作,所以num的值会>=100,并且具体的数值每次执行都可能不同。
案例6
- (void)test4 {
self.num = 0;
for (int i = 0; i < 100; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.num ++;
});
}
NSLog(@"self.num = %d",self.num);
}
复制代码
for循环执行100次,每次使用异步函数对num进行++。当for执行到100次时,不能确定有多少个异步函数的任务已经被执行,此时打印num,可能还有异步函数里的任务还未被执行,所以num应该是<=100。