多线程(中)之死锁、同步函数和异步函数

GCD死锁

我们知道,当在主线程执行如下代码时,会出现死锁。

dispatch_sync(dispatch_get_main_queue(), ^{
    
});
复制代码

那出现死锁的原因又到底是因为什么呢?

首先运行这段代码,当出现崩溃时,查看堆栈。

image.png

我们可以发现,崩溃的时候是在__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_valuetid又代表着什么呢?

参数传递进来的是(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_statenew_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_valuetid必须是相等的,根据刚才的分析,这两个值是当前队列的等待状态和当前线程的等待状态,所以当二者都是等待状态的时候,发生死锁。这与众所周知的死锁原因是一致的。

最后用一句话来总结一下:在同个线程里,同步向串行队列里面添加任务,造成任务相互等待,就会死锁。

死锁分析案例

案例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的地方有多处,光看源码并不能知道代码执行的流程,那么接下来就通过符号断点的方式来探索一下。

首先使用一个全局队列,并打上断点。 image.png 接下来将_dispatch_sync_f函数中所有使用到func的函数都打上符号断点后继续执行,发现执行了_dispatch_sync_f_slow函数。 image.png 那就接着查看_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函数。 image.png 查看_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中执行,所以调用同步函数的时候,必须得等同步函数的任务执行完成以后,才能继续执行其他的代码。

那么我们就可以总结出同步函数的特点。

  1. 立即执行
  2. 阻塞当前线程
  3. 不具备开辟子线程的能力

异步函数

看完同步函数之后,我们再来看看异步函数。首先先看看源码。

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中,把workf都封装到了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)
复制代码

再往后看并没有发现一些有用的代码,只看源码我们并不能知道流程的来龙去脉,那就换个思路。

我们写一个异步函数的调用,然后打上断点,再查看堆栈信息。

image.png 通过断点我们发现了,当使用异步函数的时候,代码块并不是在主线程(Thread1)中执行的,而是在Thread5执行的,我们打印一下当前线程,线程的number=6(number从1开始,主线程number=1)。并且在堆栈信息中,我们发现最终异步函数也是调用了_dispatch_client_callout函数,这与同步函数最终的执行也是一样的,只不过同步函数是在主线程执行的。

那么我们也可以总结出异步函数的特点

  1. 可以开辟子线程,在子线程中执行任务
  2. 不会立即执行
  3. 不会阻塞当前线程

面试题

案例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。

猜你喜欢

转载自juejin.im/post/7107265602728755230