dispatch_sync死锁问题研究

首先,看看如下代码的输出是什么?

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"Hello");
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"World");
            });
}

首先答案是会发生死锁,我们看看官方文档关于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

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的实现

source/queue.c

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()函数体中。

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

source/queue.c

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来执行它。死锁愉快的产生了。

最后:

我们绘制上张图来轻松的描述一下这个问题:

dispatch_sync.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 首先,看看如下代码的输出是什么? - (void)viewDidLoad { [superviewDidLoad...
    made_China阅读 840评论 0 2
  • 简介 GCD(Grand Central Dispatch)是在macOS10.6提出来的,后来在iOS4.0被引...
    sunmumu1222阅读 803评论 0 2
  • 讲清这个问题需要理解以下几个基本知识 线程是什么? GCD中队列与任务是什么,sync和async方法是什么样的机...
    minking1982阅读 1,181评论 1 4
  • 一. 重点: 1.dispatch_queue_create(生成Dispatch Queue) 2.Main D...
    BestJoker阅读 1,544评论 2 2
  • 人生的枷锁就是追求百分之百的安全感。 当我们追求百分之百安全感的时候,我们没有办法集中精力去深入地思考,长期地思考...
    WindyLiu阅读 569评论 4 3