第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

posted @ 2017-08-22 23:14  鳄鱼不怕牙医不怕  阅读(336)  评论(0编辑  收藏  举报