GCD 入门(四)杂项补遗

Mike Ash Friday Q&A 中文译文:GCD 入门(四)杂项补遗

作者 TommyWu
封面圖片: GCD 入门(四)杂项补遗

译文 · 原文: Friday Q&A 2009-09-18: Intro to Grand Central Dispatch, Part IV: Odds and Ends · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2009-09-18-intro-to-grand-central-dispatch-part-iv-odds-and-ends.html 发布:2009-09-18 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


又是每周的这个时间了。在过去的三周里,我已经向您介绍了 Grand Central Dispatch 的主要组成部分 —— 这是 Snow Leopard 中一个用于并行处理和事件处理的令人兴奋的新 API。第一周我介绍了基本概念和 dispatch queue(调度队列)。第二周我讨论了如何使用 dispatch queue 在多核计算机上进行并行处理。第三周我介绍了 GCD 的事件处理系统。本周我将介绍之前未涉及的各种零散内容:dispatch queue 的挂起与 targeting(目标设置)、信号量以及一次性初始化。

与前几周一样,我假设您在阅读本文之前已经阅读了所有之前的文章,因此熟悉迄今为止讨论的 GCD 的所有方面。如果您还没有阅读那些文章,请现在就去阅读。

Dispatch Queue 的挂起

Dispatch queue 可以随意挂起和恢复。要挂起,请使用 dispatch_suspend 函数;要恢复,请使用 dispatch_resume。它们的运作方式基本符合您的预期。请注意,这些函数也适用于 dispatch sources(调度源)。

关于派发队列(dispatch queue)挂起需要注意的一点是,挂起操作的粒度是块(block)级别的。换句话说,挂起一个队列并不会立即中断当前正在执行的块。实际发生的情况是:当前块会运行直至完成,之后在队列(或源)恢复之前,将不再允许执行任何后续的块。

最后引用手册页的一句话:如果之前已经挂起了队列 / 源,则在销毁之前必须将其恢复。

Dispatch Queue Targeting(派发队列目标)
所有自定义派发队列都有一个目标队列(target queue)的概念。本质上,自定义派发队列本身并不执行任何工作,而是将工作传递给其目标队列来执行。通常,自定义队列的目标队列是默认优先级的全局队列(default-priority global queue)。

自定义队列的目标队列(target queue)可以通过 dispatch_set_target_queue 函数来设置。你可以将任何其他调度队列(dispatch queue)传递给它,甚至是另一个自定义队列,只要不创建循环依赖即可。此函数可用于通过简单地将自定义队列的目标设置为不同的全局队列(global queue)来设定其优先级。若将自定义队列的目标设为低优先级全局队列,该自定义队列上的所有任务都将以低优先级执行,高优先级全局队列的情况亦然。

该函数的另一个潜在用途是将自定义队列的目标指向主队列(main queue)。这将导致所有提交到该自定义队列的块都在主线程上运行。这样做的优势(而非直接使用主队列)在于,你的自定义队列可以独立地被挂起(suspend)和恢复(resume),并且之后可能被重新指向某个全局队列 —— 尽管你必须小心确保此后运行的所有块都能适应离开主线程的环境!

另一个潜在用途是将自定义队列(custom queues)指向其他自定义队列。这将强制多个队列彼此序列化,并本质上创建了一组队列,这些队列可以通过挂起 / 恢复它们所指向的队列来一起挂起和恢复。为了说明可能的使用方式,想象一个应用程序正在扫描一组目录并加载其中的文件。为了避免磁盘争用,您希望确保每个物理磁盘只有一个文件加载任务处于活动状态。然而,可以从物理上分离的磁盘同时读取多个文件。要实现这一点,您只需构建一个映射磁盘结构的调度队列(dispatch queue)结构。

首先,您会扫描系统并找到磁盘,为每个磁盘创建一个自定义调度队列(dispatch queue)。然后,您会扫描文件系统,并为每个文件系统也创建一个自定义队列,将它们的目标队列(target queues)指向相应磁盘的队列。最后,每个目录扫描器也可以有自己的队列,将其目标指向目录所在的文件系统。目录扫描器可以枚举其目录,并为每个文件提交一个块(block)直接到它们自己的队列。由于系统的设置方式,这将固有地序列化对每个物理磁盘的所有访问,同时允许并行访问不同的磁盘,所有这一切都无需在初始队列设置过程之外进行任何手动干预。

信号量(Semaphores) Dispatch 信号量的工作方式与其他任何信号量一样,如果您熟悉其他多线程系统中的信号量,这看起来对您来说将完全熟悉。

信号量(semaphore)基本上是一个整数,它有一个初始计数并支持两个操作:signal 和 wait。当一个信号量被 signal 时,计数会增加。当一个线程在信号量上等待时,它会阻塞,如果必要的话,直到计数大于 0,然后递减计数。

Dispatch 信号量使用 dispatch_semaphore_create 创建,使用 dispatch_semaphore_signal 发送信号,使用 dispatch_semaphore_wait 等待。这些函数的 man page 展示了两个如何使用信号量的好例子,一个涉及同步工作完成,另一个涉及控制对有限资源的访问。与其编造我自己的、次优的例子,我鼓励你简单地阅读 man page 来查看这些对象的一些潜在用途。

一次性初始化 GCD 也支持一次性初始化,如果你熟悉 pthreads(POSIX 线程),这基本上与 pthread_once 调用相同。GCD 方法的主要优势是它使用 blocks(块)而不是函数指针(function pointers),从而允许更自然的代码流。

该技术的一个主要用途是以线程安全的方式实现单例或其他共享数据的延迟初始化。对于需要线程安全的单例,典型的初始化技术如下所示:

+ (id)sharedWhatever
{
static Whatever *whatever = nil;
@synchronized([Whatever class])
{
if(!whatever)
whatever = [[Whatever alloc] init];
}
return whatever;
}

使用 GCD(Grand Central Dispatch)你可以重写上述方法,使用dispatch_once如下:

+ (id)sharedWhatever
{
static dispatch_once_t pred;
static Whatever *whatever = nil;
dispatch_once(&pred, ^{
whatever = [[Whatever alloc] init];
});
return whatever;
}

结论

至此,关于 Grand Central Dispatch(大中央调度)的系列内容就结束了。本周你了解了如何挂起、恢复和重定向派发队列(dispatch queues),以及这些功能的用途。你还学习了如何使用派发信号量(dispatch semaphores)和一次性初始化设施。在前几周中,你已经掌握了如何管理派发对象(dispatch objects),如何创建、访问和使用适用于不同任务的各种派发队列,利用多核系统(multicore systems)的策略,以及如何使用 GCD 的事件系统来监视事件。现在,你对 GCD 的工作原理和使用方法已经有了完整的认识,那就去用它来编写一些出色的新软件吧!

下周请回来收看另一期精彩的 Friday Q & A。一如既往,如果你有任何话题建议,请在评论区留言或直接通过电子邮件发送给我。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2009-09-18-intro-to-grand-central-dispatch-part-iv-odds-and-ends.html

It’s that time of the week again. Over the past three weeks I’ve introduced you to the major pieces Grand Central Dispatch, an exciting new API for parallel processing and event handling in Snow Leopard. The first week I covered basic concepts and dispatch queues. The second week I discussed how to use dispatch queues for parallel processing on multi-core computers. The third week I covered GCD’s event handling system. This week I’m going to cover various odds and ends which I didn’t get to before: dispatch queue suspension and targeting, semaphores, and one-time initialization.

As with the previous weeks, I will assume that you’ve read all of the previous articles before reading this one, and are thus familiar with all aspects of GCD discussed up to this point. If you have not already read those articles, please do so now.

Dispatch Queue Suspension Dispatch queues can be suspended and resumed at will. To suspend, use the dispatch_suspend function, and to resume, use dispatch_resume. These work pretty much the way you’d expect them to. Note that they also work on dispatch sources.

One caveat to dispatch queue suspension is that suspension is block-granular. In other words, suspending a queue does not suspend the currently executing block. Instead what happens is that the block is allowed to run to completion, and then no further blocks are allowed to run until the queue (or source) is resumed.

One final note, straight from the man page: you must resume a queue/source before destroying it if you have previously suspended it.

Dispatch Queue Targeting All custom dispatch queues have the concept of a target queue. In essence, a custom dispatch queue doesn’t actually execute any work, but passes work to its target queue for execution. Normally the target queue of a custom queue is the default-priority global queue.

The target queue of a custom queue can be set by using the dispatch_set_target_queue function. You can pass it any other dispatch queue, even another custom queue, so long as you never create a cycle. This function can be used to set the priority of a custom queue by simply setting its target queue to a different global queue. If you set your custom queue’s target to be the low priority global queue, all work on your custom queue will execute with low priority, and the same with the high priority global queue.

Another potential use of this is to target a custom queue to the main queue. This will cause all blocks submitted to that custom queue to run on the main thread. The advantage of doing this instead of simply using the main queue directly is that your custom queue can be independently suspended and resumed, and could potentially be retargeted onto a global queue afterwards, although you’ll have to be careful to ensure that all blocks which run after that point can tolerate being away from the main thread!

Yet another potential use is to target custom queues to other custom queues. This will force multiple queues to be serialized with respect to each other, and essentially creates a group of queues which can all be suspended and resumed together by suspending/resuming the queue that they target. For a way that this could be used, imagine an application which is scanning a set of directories and loading the files within. In order to avoid disk contention, you want to make sure that only one file loading task is active for each physical disk. However, multiple files can be read from physically separate disks simultaneously. To make this happen, all you have to do is build a dispatch queue structure which mirrors your disk structure.

First, you would scan the system and find the disks, and create a custom dispatch queue for each one. Then you would scan the filesystems and create a custom queue for each one of those as well, pointing their target queues at the queue of the appropriate disk. Finally, each directory scanner can have its own queue as well, pointing the target at the filesystem on which the directory resides. The directory scanners can enumerate their directories and submit a block for each file directly to their own queue. Due to how the system is set up, this will inherently serialize all access to each physical disk while allowing parallel access to separate disks, all without any manual intervention beyond the initial queue setup process.

Semaphores Dispatch semaphores work just like any other semaphore and if you’re familiar with semaphores from other multithreading systems, this will look entirely familiar to you.

A semaphore is basically an integer which has an initial count and supports two operations: signal and wait. When a semaphore is signaled, the count is incremented. When a thread waits on a semaphore, it will block, if necessary, until the count is greater than 0, and then decrement the count.

Dispatch semaphores are created using dispatch_semaphore_create, signaled using dispatch_semaphore_signal, and waited on using dispatch_semaphore_wait. The man page for these functions shows two good examples of how to use semaphores, one involving synchronizing work completion and one involving controlling access to a finite resource. Rather than make up my own, inferior examples, I encourage you to simply read the man page to see some potential uses for these objects.

One-Time Initialization GCD also has support for one-time initialization, which if you’re familiar with pthreads is pretty much the same as the pthread_once call. The main advantage of the GCD approach is that it uses blocks instead of function pointers, allowing for a more natural flow of code.

A major use of this is for lazily initializing singletons or other shared data in a thread-safe manner. The typical singleton initialization technique looks like this, for singletons that need to be thread safe:

+ (id)sharedWhatever
{
static Whatever *whatever = nil;
@synchronized([Whatever class])
{
if(!whatever)
whatever = [[Whatever alloc] init];
}
return whatever;
}

Using GCD you can rewrite the above method using dispatch_once like so:

+ (id)sharedWhatever
{
static dispatch_once_t pred;
static Whatever *whatever = nil;
dispatch_once(&pred, ^{
whatever = [[Whatever alloc] init];
});
return whatever;
}

Conclusion That wraps up this series on Grand Central Dispatch. This week you saw how to suspend, resume, and retarget dispatch queues, and some uses for these facilities. You also saw how to use dispatch semaphores and one-time initialization facilities. In previous weeks you saw how to manage dispatch objects, how to create/access and use the different types of dispatch queues available for different tasks, strategies for taking advantage of multi-core systems, and how to monitor for events using GCD’s events system. Now you have the complete picture of how GCD operates and how to use it, so go out there and write some great new software with it!

Come back next week for another exciting edition of Friday Q&A. As always, if you have a suggestion for a topic to cover, please post it in the comments or e-mail it directly to me.