dispatch_sync 死锁问题解释

讲清这个问题需要理解以下几个基本知识

  1. 线程是什么?
  2. GCD中队列与任务是什么,sync和async方法是什么样的机制?

这两个问题参考----线程,GCD,runloop(1)

有了1,2两个问题的理解,我们从纯理解的意义上就可以解释主线程调用dispatch_sync到主队列就好理解了。
问题代码

dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"1------%@",[NSThread currentThread]);
        }
    });

主要原因是:主线程此时正在处理当前队列,并且阻塞在dispatch_sync,而dispatch_sync函数又将一个新的任务提交到主队列排队执行,然后主线程这个时候要处理完当前任务才能取出新的任务进行执行,这样导致死锁

dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_sync(queue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"1------%@",[NSThread currentThread]);
        }
    });

以上在主线程同步执行不会造成死锁
原因是: 主线程运行到dispatch_sync同样阻塞,但这个时候dispatch_sync将任务提交到并行队列,主线程可以从并行队列中取出任务执行,执行完任务后dispatch_sync返回,不会造成死锁。
再看看官方解释

Submits a block to a dispatch queue like dispatch_async(), however
dispatch_sync() will not return until the block has finished.

Calls to dispatch_sync() targeting the current queue will result
in dead-lock. Use of dispatch_sync() is also subject to the same
multi-party dead-lock problems that may result from the use of a mutex.
Use of dispatch_async() is preferred.

Unlike dispatch_async(), no retain is performed on the target queue. Because
calls to this function are synchronous, the dispatch_sync() "borrows" the
reference of the caller.

As an optimization, dispatch_sync() invokes the block on the current
thread when possible.

如果dispatch_sync()的目标queue为当前queue,会发生死锁(并行queue并不会)。使用dispatch_sync()会遇到跟我们在pthread中使用mutex锁一样的死锁问题。

以上解释如果仍然不够清楚,那么我们从代码层面去解释如下

source/queue.c中窥探dispatch_sync源代码

void
dispatch_sync(dispatch_queue_t dq, void (^work)(void))
{
    struct Block_basic *bb = (void *)work;
    dispatch_sync_f(dq, work, (dispatch_function_t)bb->Block_invoke);
}

DISPATCH_NOINLINE
void
dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func)
{
    typeof(dq->dq_running) prev_cnt;
    dispatch_queue_t old_dq;

    if (dq->dq_width == 1) {
        return dispatch_barrier_sync_f(dq, ctxt, func);
    }

    // 1) ensure that this thread hasn't enqueued anything ahead of this call
    // 2) the queue is not suspended
    if (slowpath(dq->dq_items_tail) || slowpath(DISPATCH_OBJECT_SUSPENDED(dq))) {
        _dispatch_sync_f_slow(dq);
    } else {
        prev_cnt = dispatch_atomic_add(&dq->dq_running, 2) - 2;

        if (slowpath(prev_cnt & 1)) {
            if (dispatch_atomic_sub(&dq->dq_running, 2) == 0) {
                _dispatch_wakeup(dq);
            }
            _dispatch_sync_f_slow(dq);
        }
    }

    old_dq = _dispatch_thread_getspecific(dispatch_queue_key);
    _dispatch_thread_setspecific(dispatch_queue_key, dq);
    func(ctxt);
    _dispatch_workitem_inc();
    _dispatch_thread_setspecific(dispatch_queue_key, old_dq);

    if (slowpath(dispatch_atomic_sub(&dq->dq_running, 2) == 0)) {
        _dispatch_wakeup(dq);
    }
}

Step1. 可以看到dispatch_sync将我们block函数指针进行了一些转换后,直接传给了dispatch_sync_f()去处理。

Step2. dispatch_sync_f首先检查传入的队列宽度(dq_width),由于我们传入的main queue为串行队列,队列宽度为1,所有接下来会调用dispatch_barrier_sync_f,传入3个参数,dispatch_sync中的目标queue、上下文信息和由我们block函数指针转化过后的func结构体。

接下来我们看看dispatch_barrier_sync_f的实现

void
dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func)
{
    dispatch_queue_t old_dq = _dispatch_thread_getspecific(dispatch_queue_key);

    // 1) ensure that this thread hasn't enqueued anything ahead of this call
    // 2) the queue is not suspended
    // 3) the queue is not weird
    if (slowpath(dq->dq_items_tail)
            || slowpath(DISPATCH_OBJECT_SUSPENDED(dq))
            || slowpath(!_dispatch_queue_trylock(dq))) {
        return _dispatch_barrier_sync_f_slow(dq, ctxt, func);
    }

    _dispatch_thread_setspecific(dispatch_queue_key, dq);
    func(ctxt);
    _dispatch_workitem_inc();
    _dispatch_thread_setspecific(dispatch_queue_key, old_dq);
    _dispatch_queue_unlock(dq);
}

Step3. disptach_barrier_sync_f首先做了做了3个判断:

队列存在尾部节点状态(判断当前是不是处于队列尾部)
队列不为暂停状态
使用_dispatch_queue_trylock检查队列能被正常加锁。
满足所有条件则不执行if语句内的内容,执行下面代码,简单解释为:

使用mutex锁,获取到当前进程资源锁。
直接执行我们block函数指针的具体内容。
然后释放锁,整个调用结束。
然后在我们例子中,很显然当前队列中还有其他viewController的任务,我们的流程跑到_dispatch_barrier_aync_f_slow()函数体中。

刨根问底,让我们看看这个函数。

static void
_dispatch_barrier_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func)
{

    // It's preferred to execute synchronous blocks on the current thread
    // due to thread-local side effects, garbage collection, etc. However,
    // blocks submitted to the main thread MUST be run on the main thread

    struct dispatch_barrier_sync_slow2_s dbss2 = {
        .dbss2_dq = dq,
#if DISPATCH_COCOA_COMPAT
        .dbss2_func = func,
        .dbss2_ctxt = ctxt,
#endif
        .dbss2_sema = _dispatch_get_thread_semaphore(),
    };
    struct dispatch_barrier_sync_slow_s {
        DISPATCH_CONTINUATION_HEADER(dispatch_barrier_sync_slow_s);
    } dbss = {
        .do_vtable = (void *)DISPATCH_OBJ_BARRIER_BIT,
        .dc_func = _dispatch_barrier_sync_f_slow_invoke,
        .dc_ctxt = &dbss2,
    };
//---------------重点是这里---------------    
    _dispatch_queue_push(dq, (void *)&dbss);
    dispatch_semaphore_wait(dbss2.dbss2_sema, DISPATCH_TIME_FOREVER);
    _dispatch_put_thread_semaphore(dbss2.dbss2_sema);

#if DISPATCH_COCOA_COMPAT
    // Main queue bound to main thread
    if (dbss2.dbss2_func == NULL) {
        return;
    }
#endif
    dispatch_queue_t old_dq = _dispatch_thread_getspecific(dispatch_queue_key);
    _dispatch_thread_setspecific(dispatch_queue_key, dq);
    func(ctxt);
    _dispatch_workitem_inc();
    _dispatch_thread_setspecific(dispatch_queue_key, old_dq);
    dispatch_resume(dq);
}

Step4. 既然我们上面已经判断了,main queue中还有其他任务,现在不能直接执行这个block,跳入到_dispatch_barrier_sync_f_slow函数体,那它怎么处理我们加入的block呢?

在_dispatch_barrier_sync_f_slow中,使用_dispatch_queue_push将我们的block压入main queue的FIFO队列中,然后等待信号量,ready后被唤醒。

然后dispatch_semaphore_wait返回_dispatch_semaphore_wait_slow(dsema, timeout)函数,持续轮训并等待,直到条件满足。

所以在此过程中,我们最初调用的dispatch_sync函数一直得不到返回,main queue被阻塞,而我们的block又需要等待main queue来执行它。死锁愉快的产生了。

上张图来描述一下这个问题.

希望可以相互交流切磋. 附上我的微信号关注我哈

发布了17 篇原创文章 · 获赞 6 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/jacky_jin/article/details/104115725
今日推荐