第44条:通过 Dispatch Group 机制,根据系统资源状况来执行任务
本条要点:(作者总结)
- 一系列任务可归入一个 dispatch group 之中。开发者可以在这组任务执行完毕时获得通知。
- 通过 dispatch group ,可以在并发式派发队列里同时执行多项任务。此时 GCD 会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码。
dispatch group(意为“派发分组”或“调度组”) 是 GCD 的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。这个功能有许多用途,其中最重要、最值得注意的用法,就是把将要并发执行的多个任务合为一组,于是调用者就可以知道这些任务何时才能全部执行完毕。比方说,可以把压缩一系列文件的任务表示成 dispatch group。
下面这个函数可以创建 dispatch group:
1 dispatch_group_t dispatch_group_create();
dispatch group 就是个简单的数据结构,这种结构彼此之间没什么区别,它不像派发队列,后者还有个用来区别身份的标识符。想把任务编组,有两种办法。第一种是用下面这个函数:
1 void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
它是普通 dispatch_async 函数的变体,比原来多一个参数,用于表示待执行的块所归属的组。还有种办法能够指定任务所属的 dispatch group,那就是使用下面这一对函数:
1 void dispatch_group_enter(dispatch_group_t group); 2 3 void dispatch_group_leave(dispatch_group_t group);
前者能够使分组里正要执行的任务数递增,而后者则使之递减。由此可知,调用了 dispatch_group_enter 以后,必须有与之对应的 dispatch_group_leave 才行。这与引用计数相似,要使用引用计数,就必须令保留操作与释放操作彼此对应,以防内存泄漏。而在使用 dispatch group 时,如果调用 enter 之后,没有相应的 leave 操作,那么这一组任务就永远执行不完。
下面这个函数可用于等待 dispatch group 执行完毕:
1 long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
此函数接受两个参数,一个是要等待的 group,另一个是代表等待时间的 timeout 值。timeout 参数表示函数在等待 dispatch group 执行完毕时,应该阻塞多久。如果执行 dispatch group 所需的时间小于 timeout,则返回 0, 否则返回非 0 值。此参数也可以取常量 DISPATCH_TIME_FOREVER,这表示函数会一直等着 dispatch group 执行完。而不会超时(time out)。
除了可以用上面那个函数等待 dispatch group 执行完毕之外,也可以换个办法,使用下列函数:
1 void dispatch_group_notiy(dispath_group_t group, dispatch_queue_t queue, dispath_block_t block);
与 wait 函数略有不同的是:开发者可以向此函数传入块,等 dispatch group 执行完毕之后,块会在特定的线程上执行。假如当前线程不应阻塞,而开发者又想在那些任务全部完成时得到通知,那么此做法就很有必要了。比方说,在 Mac OS X 与 iOS 系统中,都不应阻塞主线程,因为所有 UI 绘制及事件处理都要在主线程上执行。
如果想令数组中的每个对象都执行某项任务,并且想等待所有任务执行完毕,那么就可以使用这个 GCD 特性来实现。代码如下:
1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCh_QUEUE_PRIORITY_DEFAULT, 0); 2 3 dispath_group_t dispatchGroup = dispatch_group_create(); 4 5 for (id object in collection) { 6 dispatch_group_async(dispatchGroup, queue, ^{ 7 8 [object performTask]; 9 }); 10 11 } 12 dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER); 13 14 // Continue processing after completing task
若当前线程不应阻塞,则可用 notify 函数来取代 wait:
1 dispatch_queue_t notifyQueue = dispatch_get_main_queue(); 2 3 dispatch_group_notify(dispatchGroup, notifyQueue, ^{ 4 5 // Continue processing after completing tasks 6 });
notify 回调时所选用的队列,完全应该根据具体情况来定。笔者在范例代码中使用了主队列,这是中常见写法。也可以用自定义的串行队列或全局并发队列。
在本例中,所有任务都派发到同一个队列之中。但实际上未必一定要这样做。也可以把某些任务放在优先级高的线程上执行,同时仍然把所有任务都归入同一个 dispatch group。并在执行完毕时获得通知:
1 dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); 2 3 dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); 4 5 dispatch_group_t dispatchGroup = dispatch_group_create(); 6 7 for (id object in lowPriorityObjects) { 8 9 dispatch_group_async(dispatchGroup, lowPriorityQueue, ^{ 10 [object performTask]; 11 }); 12 } 13 14 for (id object in highPriorityObjects) { 15 16 dispatch_group_async(dispatchGroup, highPriorityQueue, ^{ 17 [object performTask]; 18 }); 19 } 20 21 dispatch_queue_t notifyQueue = dispatch_get_main_queue(); 22 23 dispatch_group_notify(dispatchGroup, notifyQueue, ^{ 24 25 // Continue processing after completiing tasks 26 });
除了像上面这样把任务提交到并发队列之外,也可以把任务提交至各个串行队列中,并用 dispatch group 跟踪其执行状况。然而,如果所有任务都排在同一个串行队列里面,那么 dispatch group 就用处不大了。因为此时任务总要逐个执行,所以只需在提交完全部任务之后再提交一个块即可,这样做与通过 notify 函数等待 dispatch group 执行完毕然后再回调块是等效的:
1 dispatch_queue_t queue = dispatch_queue_create("com.effectiveobjectivec.queue", NULL); 2 3 for (id object in collection) { 4 5 dispatch_async(queue, ^{ 6 7 [object performTask]; 8 9 }); 10 } 11 12 dispatch_async(queue, ^{ 13 14 // Continue processing after completin tasks 15 });
上面这段代码表明,开发者未必总需要使用 dispatch group。有时候采用单个队列搭配标准的异步派发,也可以实现相同效果。
笔者为何要在标题中谈到“根据系统资源状况来执行任务”呢? 回头看看向并发队列派发任务的那个例子,就会明白了。为了执行队列中的块,GCD 会在适当的时机自动创建新线程或复用旧线程。如果使用并发队列,那么其中有可能会有多个线程,这也意味着多个块可以并发执行。在并发队列中,执行任务所用的并发线程数量,取决于各种因素,而GCD 只要是根据系统资源状况来判定这些因素的。假如 CPU 有多个核心,并且队列中有大量任务等待执行,那么GCD 就可能会给该队列配备多个线程。通过 dispatch group 所提供的这种简便方式,既可以并发执行一系列给定的任务,又能在全部任务结束时得到通知。由于 GCD 有并发队列机制,所以能够根据可用的系统资源状况来并发执行任务。而开发者则可以专注于业务逻辑代码,无须再为了处理并发任务而编写复杂的调度器。
在前面的范例代码中,我们遍历某个 collectioin,并在其每个元素上执行任务,而这也可以用另外一个 GCD 函数来实现:
1 void dispatch_apply(size_t iterations, dispatch_queue_t queue, void^(block)(size_t));
此函数会将块反复执行一定的次数,每次传给块的参数值都会递增,从0 开始,直至 “iterations-1”。其用法如下:
1 dispatch_queue_t queue = dispatch_queue_create("com.effectiveobjectivec.queue", NULL); 2 3 dispatch_appley(10, queue, ^(size_t i) { 4 5 // Perform task 6 });
采用简单的 for 循环,从 0 递增至 9,也能实现同样效果:
1 for (int i = 0; i < 10 ; i++) { 3 // Perform task 5 }
有一件事要注意:dispatch_apply 所用的队列可以是并发队列。如果采用并发队列,那么系统就可以根据资源状况来并行执行这些块了,这与使用 dispatch group 的那段范例代码一样。上面这个 for 循环要处理的 collection 若是数组,则可用 dispatch_apply 改写如下:
1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 2 3 dispatch_apply(array.count, queue, ^(size_t i) { 4 5 id object = array[i]; 6 [object performTask]; 7 });
这个例子再次表明:未必总要使用 dispatch group。然而,dispatch_apply 会持续阻塞直到所有任务都执行完毕为止。由此可见: 假如把块派给了当前队列(或者体系中高于当前队列的某个串行队列),就将导致死锁。若想在后台执行任务,则应使用 dispatch group。
END