在计算机发展初期,计算机可以执行的每单位时间的最大工作量取决于CPU的频率。但随着技术进步和处理器设计变得更加紧凑,热量和其他物理约束开始限制处理器的最大频率。因此,芯片制造商寻找其他方法来提高芯片的整体性能。他们的解决方案是增加每个芯片上处理器内核的数量。通过增加内核数量,单个芯片每秒可以执行更多指令,而不会增加CPU频率或者改变芯片尺寸或热特性。唯一的问题是如何利用额外的内核。
为了利用多个内核,计算机需要能够同时完成多项任务的软件。对于像OS X或者iOS这样的现代多任务操作系统,在任何给定的时间都可以运行一百或者更多个程序,因此将每个程序安排到不同的内核中应该是可能的。然而,这些程序中的大多数都是系统守护进程和后台应用程序,这些应用程序实际处理时间非常短。相反,真正需要的是个别应用程序更有效地利用额外内核的方式。
所以,为了总结这个问题,需要一种方法让应用程序利用可变数量的计算机内核。单个应用程序执行的工作量也需要能够动态扩展以适应不断变化的系统条件。解决方案必须足够简单,以免增加利用这些内核所需的工作量。好消息是苹果的操作系统为所有这些问题提供了解决方案,本章将介绍构成该解决方案的技术以及可以对代码进行设计调整以利用它们。
抛弃线程
虽然线程已经存在了很多年,并且仍然有其用处,但它们并没有解决以可扩展的方式执行多个任务的一般问题。使用线程,创建可扩展解决方案的负担落在了开发者的肩膀上。开发者必须决定创建多少个线程并根据系统条件的变化动态调整该数量。另一个问题是,应用程序承担与创建和维护其使用的任何线程相关的大部分代价。
OS X和iOS是采用异步设计方法来解决并发问题的,不是依赖线程。异步函数已经存在于操作系统中很多年了,通常用于执行可能需要很长时间的任务,例如从磁盘中读取数据。异步函数被调用时,会在后台做一些工作来开始执行一个任务,但在该任务实际完成之前就返回。在过去,如果一个异步函数不存在你想要做的事情,开发者将不得不编写自己的异步函数并创建自己的线程。但是现在,OS X和iOS提供的技术允许开发者异步执行任何任务,而无需自己管理线程。
Grand Central Dispatch (GCD) 是异步执行任务的技术之一。该技术采用开发者在应用程序中编写的线程管理代码,并将该代码移至系统级别。开发者只需要定义要执行的任务并将其添加到适当的dispatch queue(调度队列)中即可。GCD负责创建所需的线程并安排任务在这些线程上运行。由于线程管理现在是系统的一部分,因此GCD提供了一种全面的任务管理和执行方法,比传统线程提供更高的效率。
Operation queue(操作队列)是与dispatch queue非常类似的Objective-C对象。开发者定义要执行的任务,然后将其添加到operation queue中。像GCD一样,operation queue为开发者处理所有线程管理,确保在系统上尽可能快速和高效地执行任务。
以下各节描述了有关dispatch queue、operation queue以及可在应用程序中使用的其他一些有关异步技术的更多信息。
Dispatch Queue
Dispatch queue是一种基于C语言的机制,能够用来执行自定义任务。dispatch queue可以串行或并行执行任务,但始终按先进先出的顺序执行(换句话说,dispatch queue总是按照任务被添加到队列的顺序启动任务,并以相同顺序推出任务)。serial dispatch queue(串行调度队列)一次一次只运行一个任务,直到该任务完成之后才执行下一个新任务。相比之下,concurrent dispatch queue(并行调度队列)会尽可能多地运行任务,而无需等待正在运行的任务执行完毕。
Dispatch queue还有其他益处:
- 它们提供了一个直截了当和简单的编程接口。
- 它们提供自动和全面的线程池管理。
- 它们提供了协调组装的速度。
- 它们的内存效率要高得多(因为线程栈并不存储于应用程序的内存中)。
- 它们不会陷入负载下的内核。
- 将任务异步调度到dispatch queue不会死锁队列。
- 它们的伸缩性更强。
- 串行调度队列为锁和其他同步原函数提供了更高效的替代方案。
提交给dispatch queue的任务必须封装在函数或者block对象中。block对象是OS X v10.6和iOS 4.0中引入的一种C语言特性,它在概念上类似于函数指针,但有一些额外的好处。通常在其他函数或方法中定义block,以便可以从该函数或方法访问其他变量。block也能被移出栈区并复制到堆区,这是将它们提交给dispatch queue时所发生的情况。所有这些语义都可以用较少的代码实现非常动态的任务。
Dispatch queue是Grand Central Dispatch技术的一部分,是C语言运行时的一部分。有关在应用程序中使用dispatch queue的更多信息,请参看Dispatch Queue。有关block及其优点的更多信息,请参看Block。
Dispatch Source
Dispatch source(调度源)是一种基于C语言的机制,其用于异步处理特定类型的系统事件。dispatch source封装了有关特定类型系统事件的信息,并在发生该事件时将特定block对象或者函数提交给dispatch queue。可以使用dispatch source来监视以下类型的系统事件:
- Timers
- Signal handles
- Descriptor-related events
- Process-related events
- Mach port events
- Custom events that you trigger
Dispatch source是Grand Central Dispatch技术的一部分。有关使用dispatch source在应用程序中接收事件的信息,请参看Dispatch source。
Operation Queue
Operation Queue(操作队列)是concurrent dispatch queue的Cocoa同等技术,由NSOperationQueue
实现。dispatch queue总是按照先进先出的顺序执行任务,而operation queue在确定任务的执行顺序时会考虑其他因素。这些因素中最主要的是给定的任务是否取决于其他任务的完成。可以在定义任务时配置依赖关系,并可以使用它们为任务创建复杂的执行顺序图。
提交给operation queue的任务必须是NSOperation
类的实例。operation对象是一个Objective-C对象,其封装了想要执行的任务以及执行它所需要的任何数据。由于NSOperation
类本质上是一个抽象基类,因此通常会定义自定义子类来执行任务。但是,Foundation框架确实包含了一些可以创建和使用的具体子类来执行任务。
Operation对象会生成键-值观观察(KVO)通知,这是监视任务进度的有效方法。虽然operation queue总是并行执行操作,但可以使用依赖关系来确保在需要时它们被串行执行。
有关如何使用operation queue的更多信息以及如何自定义operation对象的更多信息,请参看NSOperation和NSOperationQueue。
异步设计技术
在考虑重新设计代码以支持并发之前,应该确定一下是否需要这样做。在确保主线程可以自由地响应用户事件的情况下,并发可以提高代码的响应速度。它甚至可以通过利用更多内核在相同的时间内完成更多工作来提高代码的效率。但是,它也增加了开销以及代码的整体复杂性,使得编写和调试代码变得更加困难。
因为其增加了复杂性,所以并发不是在产品周期结束时可以移植到应用程序中的功能。要做到这一点,需要仔细考虑仔细考虑应用程序执行的任务以及用于执行这些任务的数据结构。如果使用方式不正确,可能会使代码的运行速度比以前更慢,并且对用户的响应性较差。因此,在设计周期的开始阶段花点时间设定一些目标并考虑需要采取的方法是值得的。
定义应用程序的预期行为
在考虑为应用程序添加并发性之前,应该首先定义什么才是应用程序的正确行为。了解应用程序的预期行为能够在之后验证此设计。还应该了解一下在引入并发后可能带来的预期性能优势。
首先该做的第一件事是列举出应用程序执行的任务以及与每个任务关联的对象或数据结构。最初,我们可能希望从用户选择菜单项或者单击按钮执行的任务开始。这些任务提供不连续的行为,并具有明确定义的开始和结束点。还应该列举应用程序可能执行的其他类型的无需用户交互的任务,例如基于定时器的任务。
在获得高级别任务列表后,开始将每个任务进一步分解为必须采取的一系列步骤,以便成功完成任务。在这个级别上,应该主要关注需要对任何数据结构和对象进行的修改以及这些修改如何影响应用程序的整体状态。还要注意对象和数据结构之间的依赖关系。例如,如果任务涉及对对象数组进行相同的更改,则值得注意的是对一个对象的更改是否会影响任何其他对象。如果这些对象可以彼此独立地进行修改,那么这可能是可以同时进行这些修改的地方。
分解出可执行的工作单元
从我们对应用程序任务的理解中,我们应该已经能够确定代码可能从并发中受益的地方。如果更改任务重一个或者多个步骤的顺序会改变结果,则可能需要继续串行执行这些步骤。但是如果更改顺序对结果没有任何影响,则应考虑并行执行这些步骤。在这两种情况下,我们都要定义代表需要执行的一个或多个步骤的可执行工作单元。然后使用block对象或者operation对象封装这个工作单元并调度到合适的队列中。
对于我们确定的每个可执行工作单元,不用太担心正在执行的任务总量,至少在最初是如此。尽管转换线程消耗较大,但dispatch queue和operation queue的优点之一是,在许多情况下,这些成本比传统线程要小得多。因此,使用队列可以比使用线程更有效地执行更小的工作单元。当然,我们应该始终衡量实际结果并根据需要调整任务的大小,但最初不应将任务考虑太小。
确定需要的队列
现在任务已被分解为不同的工作单元并使用block对象或者operation对象进行了封装,我们需要定义要用于执行该代码的队列。对于给定的任务,请检查创建的block对象或者operation对象以及它们必须被执行的顺序,确保正确执行任务。
如果使用block来实现任务,则可以将block添加到serial dispatch queue或concurrent dispatch queue中。如果需要特定的顺序执行这些block,则应该将它们添加到serial dispatch queue中。如果不需要以特定的顺序执行,则可以将这些block添加到concurrent dispatch queue中,或根据需要将它们添加到几个不同的dispatch queue中。
如果使用operation对象来实现任务,要串行执行这些operation对象,必须配置相关对象之间的依赖关系。依赖性阻止一个operation对象执行,直到它所依赖的对象完成其工作。
提高效率的几点提示
除了简单地将代码分解为更小的任务并将其添加到队列之外,还有其他一些方法可以提高使用队列的代码的整体效率:
- 如果内存使用率是一个因素,请考虑直接在任务中计算值。 如果应用程序已经绑定了内存,现在直接计算值可能比从主内存加载缓存值更快。使用给定处理器内核的寄存器和高速缓存直接计算值比主内存要快得多。
- 提前确定串行任务,并尽可能使它们更加并发。 如果一个任务必须串行执行是因为其依赖于某个共享资源,请考虑更改体系结构来移除该共享资源。
- 避免使用锁。 在大多数情况下,dispatch queue和operation queue提供的支持不需要锁。不是使用锁来保护某些共享资源,而是指定一个串行队列(或者使用operation对象依赖性)以正确的顺序执行任务。
- 尽可能依赖系统框架。 实现并发的最好方法是利用系统框架提供的内置并发。许多框架在内部使用线程和其他技术来实现并发行为。在定义任务时,看看现有的框架是否定义了一个功能或方法能够完全实现需要的功能或方法并可以并行执行。使用该API可以节省我们的工作量,并且更有可能为我们提供最大的并发可能性。
性能影响
Operation queue,dispatch queue,和dispatch source使我们可以更轻松地同时执行更多代码。但是,这些技术并不能保证提高应用程序的效率或响应速度。我们仍然有责任以满足需求的方式来使用队列,并不该对应用程序的其他资源施加过度负担。例如,虽然可以创建10000个operation对象并将它们提交到operation queue中,但这样做会导致应用程序可能分配一个巨大的内存量,这可能会导致分页并降低性能。
在代码中引入任何数量的并发之前(无论使用队列还是线程),都应该收集一组反映应用程序当前性能的基准指标。在执行更改后,应该收集其他指标并将其与基准进行比较,以查看应用程序的整体效率是否有所提高。如果并发性的引入使应用程序运行效率降低或响应速度变慢,则应使用可用的性能检测工具来查找可能的原因。
有关性能和可用性能工具的介绍,以及指向更高级性能相关主体的链接,请参看Performance Overview。
并发和其他技术
将代码分解为模块化任务是尝试和提高应用程序并发量的最佳方式。但是,这种设计方法可能无法满足每种情况下每个应用程序的需求。根据我们的任务,可能还有其他选项可用提高应用程序的整体并发性。本节概述了作为设计的一部分可以考虑使用的其他一些技术。
OpenCL和并发
在OS X中,Open Computing Language (OpenCL)是一种基于标准的技术,用于在计算机的图形处理器上执行通用计算。如果有一个明确的应用于大型数据集的计算集,则OpenCL是一种很好的技术。例如,可以使用OpenCL对图像的像素执行滤波计算,或使用使用它一次执行对多个值的复杂数学计算。换句话说,OpenCL更适合于可以并行操作数据的问题集。
尽管OpenCL适合执行大规模数据并行操作,但不适合更通用的计算。准备并将数据和所需的工作内核传输到图形卡以使其可以通过GPU进行操作需要花费大量精力。同样,检索OpenCL生成的任何结果也需要花费大量精力。因此,与系统交互的任何任务通常都不推荐使用OpenCL。例如,不会使用OpenCL处理来自文件或网络流的数据。相反,使用OpenCL执行的工作必须更加独立,才能将其转移到图形处理器并独立计算。
有关OpenCL的更多信息以及如何使用它,请参看Mac版OpenCL Programming Guide。
何时使用线程
尽管operation queue和dispatch queue是并行执行任务的首选方式,但它们不是万能的。根据我们的应用程序,我们有时可能仍然需要创建自定义线程。如果确实需要创建自定义线程,那么应该努力创建尽可能少的线程,并且应该仅将这些线程用于无法以其他方式实现的特定任务。
线程仍然是必须实时运行的代码的好方式。Dispatch queue尽可能快地运行它们的任务,但它们不能解决实时限制。如果需要在后台运行的代码具有更多可预测的行为,那么线程仍然可以提供更好的选择。
与任何线程编程一样,应该总是明智地使用线程,并且只有在绝对有必要时才使用线程。有关线程组件的更多信息以及如何使用它们,请参看Threading Programming Guide。
网友评论