GCD的dispatch_group_t的原理及使用

什么是dispatch_group_t

dispatch_group_t是提交给队列用于异步调用的一组block。可以将队列的任务block进行分组,把队列任务关联到当前的组里面,然后还可以监听已关联的任务是否全部执行完成。dispatch_group_t可以通过dispatch_group_create函数创建:

dispatch_group_t group = dispatch_group_create();

dispatch_group_t的使用

dispatch_group_t的使用有两种方式,一种dispatch_group_enter() 和dispatch_group_leave()配合的方式;另一种是使用dispatch_group_async函数。不管哪种方式,都可以通过dispatch_group_notify函数来监听group里面的任务是否都已经执行完毕,或者可以通过dispatch_group_wait函数等待所有任务执行完毕。dispatch_group_wait和dispatch_group_notify的不同是,dispatch_group_notify可以指定所有任务完成之后执行的队列,它是异步的,不会阻塞当前线程;而dispatch_group_wait是同步的,会阻塞当前线程,但是dispatch_group_wait可以设置超时时间。下面就来看看dispatch_group_t使用示例。

dispatch_group_enter()&dispatch_group_leave()

这两个函数必须成对出现,这很类似信号量。dispatch_group_enter在任务添加之前调用,相当于告诉group要添加任务,而dispatch_group_leave则在任务结束的时候调用。以下是demo示例:

    dispatch_queue_t conQueue = dispatch_queue_create("com.hello-world.djx", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_async(conQueue, ^{
        NSLog(@"1");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(conQueue, ^{
        NSLog(@"2");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(conQueue, ^{
        NSLog(@"3");
        dispatch_group_leave(group);
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"任务完成,返回主线程");
    });
使用dispatch_group_async

这个函数直接实现了dispatch_group_enter和dispatch_group_leave的作用,以下是demo示例:

    dispatch_queue_t conQueue = dispatch_queue_create("com.hello-world.djx", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, conQueue, ^{
        NSLog(@"1");
    });
    dispatch_group_async(group, conQueue, ^{
        NSLog(@"2");
    });
    dispatch_group_async(group, conQueue, ^{
        NSLog(@"3");
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"任务完成,返回主线程");
    });

到这里的时候我们可能发现它跟栅栏dispatch_barrier_async函数很像,可以等某个队列的某几个任务都执行了在执行我们想要的操作。但还是group跟栅栏函数是不一样的,group不会阻塞当前队列,而且一个group不止可以关联一个队列,可以同时关联多个队列,下面我们就来看看group关联多个队列的用法。

可以关联多个队列任务、两种方式一起使用
    dispatch_queue_t conQueue = dispatch_queue_create("com.hello-world.djx", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();

    dispatch_group_enter(group);
    dispatch_async(conQueue, ^{
        NSLog(@"1");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(globalQueue, ^{
        NSLog(@"2");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(conQueue, ^{
        NSLog(@"3");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(globalQueue, ^{
        NSLog(@"4");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(conQueue, ^{
        NSLog(@"5");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(globalQueue, ^{
        NSLog(@"6");
        dispatch_group_leave(group);
    });
    
    dispatch_group_async(group, conQueue, ^{
        NSLog(@"7");
    });

    dispatch_group_async(group, globalQueue, ^{
        NSLog(@"8");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"任务完成,返回主线程");
    });
    NSLog(@"开始等待");
    dispatch_time_t time = dispatch_walltime(NULL, 5* NSEC_PER_SEC);
    dispatch_group_wait(group, time);
    NSLog(@"等待结束")

从demo中可以看出,group可以关联多个队列conQueque和globalQueue,而且group的两种用法dispatch_group_enter()&dispatch_group_leave()和dispatch_group_async是可以一起使用的,而且dispatch_group_notify和dispatch_group_wait并不是互斥的,他们也一样可以同时使用。这个给了我们做业务有着很多选择。

Group底层原理

这里先提出几个问题:为什么dispatch_group_enter和dispatch_group_leave要成对出现?它们是怎么关联的?以及dispatch_group_async为什么不需要enter和leave?dispatch_group_notify是如何知道所有任务都执行完成了的?
接下来通过源码分析来解释这些问题。

dispatch_group_create源码解析
dispatch_group_t
dispatch_group_create(void)
{
    return _dispatch_group_create_with_count(0);
}
static inline dispatch_group_t
_dispatch_group_create_with_count(uint32_t n)
{
    dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
            sizeof(struct dispatch_group_s));
    dg->do_next = DISPATCH_OBJECT_LISTLESS;
    dg->do_targetq = _dispatch_get_default_queue(false);
    if (n) {
        os_atomic_store2o(dg, dg_bits,
                (uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
        os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
    }
    return dg;
}

DISPATCH_VTABLE能够获取group的类信息。由于初始值n为0,所以这里的os_atomic_store2o不会被调用,所以这会dg_bits为0。dg_bits用于记录enter和leave的信息。

dispatch_group_enter源码解析

dispatch_group_enter函数:

void
dispatch_group_enter(dispatch_group_t dg)
{
    // The value is decremented on a 32bits wide atomic so that the carry
    // for the 0 -> -1 transition is not propagated to the upper 32bits.
    uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
            DISPATCH_GROUP_VALUE_INTERVAL, acquire);
    uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
    if (unlikely(old_value == 0)) {
        _dispatch_retain(dg); // <rdar://problem/22318411>
    }
    if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
        DISPATCH_CLIENT_CRASH(old_bits,
                "Too many nested calls to dispatch_group_enter()");
    }
}

os_atomic_sub_orig2o调用流程:os_atomic_sub_orig2o->os_atomic_sub_orig->_os_atomic_c11_op_orig((p), (v), m, sub, -)->atomic_fetch_sub_explicit
在enter函数里,首先会对dg->dg_bits进行减一的原子操作,用于统计enter次数。有前面创建group可知,dg->dg_bits初始值应该是为0的,所以减1之后正常情况下不会等于0,所以这里懒得分析_dispatch_retain函数了。

dispatch_group_leave源码解析

dispatch_group_leave函数:

void
dispatch_group_leave(dispatch_group_t dg)
{
    // The value is incremented on a 64bits wide atomic so that the carry for
    // the -1 -> 0 transition increments the generation atomically.
    uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
            DISPATCH_GROUP_VALUE_INTERVAL, release);
    uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);

    if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {
        old_state += DISPATCH_GROUP_VALUE_INTERVAL;
        do {
            new_state = old_state;
            if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
                new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
                new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
            } else {
                // If the group was entered again since the atomic_add above,
                // we can't clear the waiters bit anymore as we don't know for
                // which generation the waiters are for
                new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
            }
            if (old_state == new_state) break;
        } while (unlikely(!os_atomic_cmpxchgv2o(dg, dg_state,
                old_state, new_state, &old_state, relaxed)));
        return _dispatch_group_wake(dg, old_state, true);
    }

    if (unlikely(old_value == 0)) {
        DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
                "Unbalanced call to dispatch_group_leave()");
    }
}

os_atomic_add_orig2o调用流程: os_atomic_add_orig2o->os_atomic_add_orig->_os_atomic_c11_op_orig->_os_atomic_c11_op_orig->atomic_fetch_add_explicit
实际上这里是实际上就是对dg->dg_state的原子加1操作,并返回旧值old_value。用于统计leave次数。dg_state操作完成之后会就会判断enter和leave是否已经成对,如果是就会调用_dispatch_group_wake唤醒group(dispatch_group_wait),同时去异步执行notify任务。如果old_value==0,说明leave次数调用过多,会导致崩溃。

static void
_dispatch_group_wake(dispatch_group_t dg, uint64_t dg_state, bool needs_release)
{
    uint16_t refs = needs_release ? 1 : 0; // <rdar://problem/22318411>

    if (dg_state & DISPATCH_GROUP_HAS_NOTIFS) {
        dispatch_continuation_t dc, next_dc, tail;

        // Snapshot before anything is notified/woken <rdar://problem/8554546>
        dc = os_mpsc_capture_snapshot(os_mpsc(dg, dg_notify), &tail);
        do {
            dispatch_queue_t dsn_queue = (dispatch_queue_t)dc->dc_data;
            next_dc = os_mpsc_pop_snapshot_head(dc, tail, do_next);
            _dispatch_continuation_async(dsn_queue, dc,
                    _dispatch_qos_from_pp(dc->dc_priority), dc->dc_flags);
            _dispatch_release(dsn_queue);
        } while ((dc = next_dc));

        refs++;
    }

    if (dg_state & DISPATCH_GROUP_HAS_WAITERS) {
        _dispatch_wake_by_address(&dg->dg_gen);
    }

    if (refs) _dispatch_release_n(dg, refs);
}

_dispatch_group_wake函数中,会循环遍历当前group里是否有notify任务,如果有则交给_dispatch_continuation_async把block提交到队列中异步调用这个block,同时会判断有没有正在等待的group任务:

if (dg_state & DISPATCH_GROUP_HAS_WAITERS) {
        _dispatch_wake_by_address(&dg->dg_gen);
    }
dispatch_group_notify源码分析
void
dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
        dispatch_block_t db)
{
    dispatch_continuation_t dsn = _dispatch_continuation_alloc();
    _dispatch_continuation_init(dsn, dq, db, 0, DC_FLAG_CONSUME);
    _dispatch_group_notify(dg, dq, dsn);
}
static inline void
_dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
        dispatch_continuation_t dsn)
{
    uint64_t old_state, new_state;
    dispatch_continuation_t prev;

    dsn->dc_data = dq;
    _dispatch_retain(dq);

    prev = os_mpsc_push_update_tail(os_mpsc(dg, dg_notify), dsn, do_next);
    if (os_mpsc_push_was_empty(prev)) _dispatch_retain(dg);
    os_mpsc_push_update_prev(os_mpsc(dg, dg_notify), prev, dsn, do_next);
    if (os_mpsc_push_was_empty(prev)) {
        os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, release, {
            new_state = old_state | DISPATCH_GROUP_HAS_NOTIFS;
            if ((uint32_t)old_state == 0) {
                os_atomic_rmw_loop_give_up({
                    return _dispatch_group_wake(dg, new_state, false);
                });
            }
        });
    }
}

在这里会首先把当前函数的任务(block)标记位notify,因为一会所有任务执行完之后要调用。会循环(os_atomic_rmw_loop2o)监听状态,状态满足时old_state == 0执行_dispatch_group_wake去执行notify的block和唤醒group的wait。_dispatch_group_wake上面已经有分析过了。

dispatch_group_wait源码解析

dispatch_group_wait函数阻塞当前线程:

long
dispatch_group_wait(dispatch_group_t dg, dispatch_time_t timeout)
{
    uint64_t old_state, new_state;

    os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, relaxed, {
        if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
            os_atomic_rmw_loop_give_up_with_fence(acquire, return 0);
        }
        if (unlikely(timeout == 0)) {
            os_atomic_rmw_loop_give_up(return _DSEMA4_TIMEOUT());
        }
        new_state = old_state | DISPATCH_GROUP_HAS_WAITERS;
        if (unlikely(old_state & DISPATCH_GROUP_HAS_WAITERS)) {
            os_atomic_rmw_loop_give_up(break);
        }
    });

    return _dispatch_group_wait_slow(dg, _dg_state_gen(new_state), timeout);
}
static long
_dispatch_group_wait_slow(dispatch_group_t dg, uint32_t gen,
        dispatch_time_t timeout)
{
    for (;;) {
        int rc = _dispatch_wait_on_address(&dg->dg_gen, gen, timeout, 0);
        if (likely(gen != os_atomic_load2o(dg, dg_gen, acquire))) {
            return 0;
        }
        if (rc == ETIMEDOUT) {
            return _DSEMA4_TIMEOUT();
        }
    }
}

dispatch_group_wait等待的原理是它开启了一个循环,检查group任务相关的状态,如国状态满足就返回0,执行dispatch_group_wait后面的代码;如果超时就返回超时提醒,也会继续执行后面的代码。

dispatch_group_async源码解析

dispatch_group_async之所以能等同于dispatch_group_enter()和dispatch_group_leave()的操作,实际上它内部实现也是依赖这两个方法实现的:

void
dispatch_group_async(dispatch_group_t dg, dispatch_queue_t dq,
        dispatch_block_t db)
{
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC;
    dispatch_qos_t qos;

    qos = _dispatch_continuation_init(dc, dq, db, 0, dc_flags);
    _dispatch_continuation_group_async(dg, dq, dc, qos);
}

static inline void
_dispatch_continuation_group_async(dispatch_group_t dg, dispatch_queue_t dq,
        dispatch_continuation_t dc, dispatch_qos_t qos)
{
    dispatch_group_enter(dg);
    dc->dc_data = dg;
    _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}

这里是先创建一个dc,然后标记为group类型任务uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC。然后在把任务添加到队列之前调用dispatch_group_enter(dg),到这里我们其实就可以发现了,dispatch_group_async实现原理实际上就是内部也是依赖dispatch_group_enter和dispatch_group_leave函数来管理的。所以我们也很容易猜到在block任务被调用的时候,也会调用dispatch_group_leave函数来保持平衡的:

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

推荐阅读更多精彩内容