并发和应用程序设计

在计算的早期阶段,计算机可以执行的每单位时间的最大工作量取决于 CPU 的时钟速度。但随着先进技术和处理器设计变得更加紧凑,热量和其他物理限制开始限制处理器的最大时钟速度。

从线程移开

Grand Central Dispatch(GCD)是异步启动任务的技术之一。该技术采用了通常在自己的应用程序中编写的线程管理代码,并将该代码移至系统级别。你只需定义要执行的任务并将其添加到适当的调度队列中即可。GCD 负责创建所需的线程并安排您的任务在这些线程上运行。由于线程管理现在是系统的一部分,因此GCD提供了一种全面的任务管理和执行方法,比传统线程提供更高的效率。

操作队列是与调度队列非常相似的 Objective-C 对象。你可以定义要执行的任务,然后将它们添加到操作队列中,像 GCD 一样,操作队列为你处理所有线程管理,确保在系统上尽可能快速和高效地执行任务。

调度队列

调度队列是用于执行自定义任务的基于C的机制。调度队列可以串行或并行执行任务,但始终按先进先出的顺序执行。串行调度队列一次只运行一个任务,等到该任务完成之后再出列并启动一个新任务。相比之下,并发调度队列尽可能多地启动任务,而无需等待已经启动的任务完成。

调度队列还有其他好处:

  • 提供了一个简单直接的编程接口。
  • 提供自动和全面的线程池管理。

  • 提供了调谐组装的速度。

  • 的内存效率要高得多(因为线程堆栈不会留在应用程序内存中)。

  • 不会在负载下陷入内核。

  • 将任务异步派发到调度队列不会使队列死锁。

  • 在竞争中优雅地缩放。

  • 串行调度队列为锁和其他同步原语提供了更高效的替代方案。

您提交给调度队列的任务必须封装在函数或块对象中。块对象是 OS X v10.6 和 iOS 4.0 中引入的一种C语言特性,它在概念上类似于函数指针,但有一些额外的好处。在将它们提交到调度队列时,块也可以移出它们的原始范围并复制到堆上。

调度队列是Grand Central Dispatch技术的一部分,是C运行时的一部分。

调度资源

调度源是一种基于C的机制,用于异步处理特定类型的系统事件。调度源封装有关特定类型系统事件的信息,并在发生该事件时将特定块对象或函数提交给调度队列。可以使用调度源来监视以下类型的系统事件:

  • 定时器(timer)
  • 信号处理程序

  • 描述符相关的事件

  • mach port 事件

  • 你触发的自定义事件

操作队列

操作队列是并发调度队列的 Cocoa 等价物,由 NSOperationQueue 类实现。而调度队列总是按先进先出的顺序执行任务,而操作队列在确定任务的执行顺序时会考虑其他因素。这些因素中最主要的是给定的任务是否取决于其他任务的完成。你可以在定义任务时配置依赖关系,并可以使用它们为你的任务创建复杂的执行顺序图。

你提交给操作队列的任务必须是NSOperation类的实例。操作对象是一个 Objective-C 对象,它封装了你想要执行的工作以及执行它所需的任何数据。由于 NSOperation 类实质上是一个抽象基类,因此通常会定义自定义子类来执行你的任务。Foundation 框架确实包含了一些可以创建和使用的具体子类来执行任务(例如,NSBlockOperation)。

操作对象生成键值观察(KVO)通知,这可能是监视任务进度的有用方法。虽然操作队列总是同时执行操作,但您可以使用依赖关系来确保在需要时它们被串行执行。

异步设计技术

通过确保主线程可以自由地响应用户事件,并发可以提高代码的响应速度。它甚至可以通过利用更多内核在相同的时间内完成更多工作来提高代码的效率。但是,它也增加了开销并增加了代码的整体复杂性,使得编写和调试代码变得更加困难。

定义应用程序的预期行为

在考虑向应用程序添加并发性之前,应该首先定义你认为是应用程序的正确行为。了解你的应用程序的预期行为可以让你稍后验证你的设计。它还应该让你了解一下通过引入并发可能会带来的预期性能优势。

你应该做的第一件事是枚举应用程序执行的任务以及与每个任务关联的对象或数据结构。最初,你可能希望从用户选择菜单项或单击按钮时执行的任务开始。这些任务提供不连续的行为,并具有明确定义的起点和终点。你还应该枚举应用程序可能执行的其他类型的任务,而无需用户交互,例如基于计时器的任务。

在获得高级任务列表后,开始将每项任务进一步分解为必须采取的一系列步骤,以便成功完成任务。在这个级别上,您应该主要关注您需要对任何数据结构和对象进行的修改以及这些修改如何影响应用程序的整体状态。你也应该注意对象和数据结构之间的依赖关系。例如,如果任务涉及对对象数组进行相同的更改,则值得注意的是对一个对象的更改是否会影响任何其他对象。如果这些对象可以彼此独立地进行修改,那么这可能是您可以同时进行这些修改的地方。

分解可执行的工作单元

如果更改任务中一个或多个步骤的顺序会更改结果,则可能需要串行执行这些步骤。但是,如果更改顺序对输出没有任何影响,则应考虑并行执行这些步骤。在这两种情况下,您都要定义表示要执行的一个或多个步骤的可执行工作单元。这个工作单元然后变成你使用块或操作对象封装并分派到适当的队列。

标识你需要的队列

现在您的任务已分解为不同的工作单元并使用块对象或操作对象进行封装,您需要定义要用于执行该代码的队列。对于给定的任务,检查您创建的块或操作对象以及它们必须执行的顺序才能正确执行任务。

如果您使用块实施您的任务,则可以将块添加到串行或并发调度队列中。如果需要特定的顺序,您总会将您的块添加到串行调度队列中。如果不需要特定的顺序,则可以将这些块添加到并发调度队列中,或根据您的需要将它们添加到几个不同的调度队列中。

如果你使用操作对象实现你的任务,那么队列的选择往往不如对象的配置有趣。要串行执行操作对象,必须配置相关对象之间的依赖关系。依赖性阻止一个操作执行,直到它所依赖的对象完成其工作。

提高效率的提示

除了简单地将代码分解为更小的任务并将其添加到队列之外,还有其他一些方法可以提高使用队列的代码的整体效率:

  • 如果内存使用率是一个因素,请考虑直接在您的任务中计算值。如果您的应用程序已经绑定了内存,现在直接计算值可能比从主内存加载缓存值更快。直接计算值使用给定处理器内核的寄存器和高速缓存,这比主内存快得多。

  • 提前识别串行任务,并尽可能使它们并行执行。如果一个任务必须串行执行,因为它依赖于某个共享资源,请考虑更改您的体系结构以删除该共享资源。您可能会考虑为每个需要的客户端创建资源副本,或者完全删除资源。

  • 避免使用锁。在大多数情况下,调度队列和操作队列提供的支持不需要锁。不要使用锁来保护某些共享资源,而是指定一个串行队列(或使用操作对象依赖性)以正确的顺序执行任务。

  • 尽可能依靠系统框架。实现并发的最好方法是利用系统框架提供的内置并发。

Performance Implications

并发和其他技术

OpenCL和并发性
何时使用线程

results matching ""

    No results matching ""