日期:2010-01-31  浏览次数:20431 次

 随着多核处理器在市场上的日益普及,它已广泛用于服务器、台式机以及便携式计算机,代码并行化的重要性也前所未有地凸显出来。 为了满足这一关键需求,Visual Studio 2010 引入了若干新的方法,帮助 C++ 开发人员利用新的并行运行时和新的并行编程模型带来的这些功能。 然而,开发人员面临的一个主要障碍是确定哪种编程模型适合于他们的应用程序。 正确的模型可以充分利用底层并行性,不过也需要重新考虑程序结构和实际的执行方式。

  目前,最常见的并行编程模型涉及到通用的并发感知容器以及并行循环迭代等算法。 虽然这些传统技术功能强大,可扩展应用程序来配合多核计算机使用,但它们并未解决影响并行性能的其他主要因素之一,那就是不断加深的延迟影响。 由于并行技术加快计算速度并将计算分布在多个内核之间,因此,Amdahl 定律 (wikipedia.org/wiki/Amdahl"s_law) 告诉我们性能改进受到执行速度最慢的那一部分制约。 在许多情况下,等待来自 I/O(例如磁盘或网络)的数据所花的时间比例越来越大。

  基于角色的编程模型能够很好地处理延迟等问题,这些模型最初是在二十世纪七十年代初引入的,目的是利用具有成百上千个独立处理器的高度并行计算机资源。 角色模型背后的基本概念是将应用程序的各个组件视为单独的角色,这些角色可以通过发送、接收和处理消息与外界交互。

  最近,随着大量多核处理器的运用,角色模型已作为一种减少延迟、实现高效并行执行的有效方法重新露面。 Visual Studio 2010 引入了异步代理库 (AAL),这是一个令人激动的基于角色的新模型,它具有消息传递接口,在该模型中代理就是角色。 AAL 使开发人员可以通过更加以数据流为中心的方式设计自己的应用程序。 这样的设计通常有利于在等待数据时有效使用延迟。

  在本文中,我们将概述 AAL 并介绍如何在应用程序中使用它。
并发运行时

  Visual Studio 2010 和 AAL 中并发支持的基础是新的并发运行时,该运行时作为 Visual Studio 2010 中 C 运行时 (CRT) 的一部分提供。 并发运行时提供协调任务计划程序和资源管理器,后者对计算机的底层资源有深入了解。 这就允许运行时以负载平衡的方式在整个多核计算机中执行任务。

  图 1 简要地展示了 Visual Studio 2010 中对本机代码并发的支持。 计划程序是确定何时何地执行任务的主要组件。 它借助资源管理器收集的信息来充分地利用执行资源。 尽管应用程序和库也可以直接与运行时交互,但它们本身主要还是通过两个位于计划程序之上的编程模型(即 AAL 和并行模式库 (PPL))与并发运行时交互。

异步代理: 使用异步代理库进行基于角色的编程

  图 1 并发运行时

  PPL 提供更为传统的并行技术(例如 parallel_for 和 parallel_for_each constructs)、可识别运行时的锁和并发数据结构(例如队列和向量)。 虽然 PPL 不是本文介绍的重点,但它也是一种功能强大的工具,开发人员可以将其与 AAL 中引入的所有新方法配合使用。 有关 PPL 的详细信息,请参阅 2009 年 2 月刊载的《使用 C++ 的 Windows》专栏 (msdn.microsoft.com/magazine/dd434652)。

  相比之下,AAL 能够在更高级别以不同于传统技术的角度来并行化应用程序。 开发人员需要从待处理数据的角度思考应用程序,并思考如何将数据处理分隔到可并行执行的组件或阶段中。

  AAL 提供两个主要组件:消息传递框架和异步代理。

  消息传递框架包括一组消息块,用于接收、处理和传播消息。 通过将消息块串连起来,可创建能够同时执行的工作管道。

  异步代理是通过接收消息、在自己维护的状态下执行本地工作和发送消息,以此与外界交互的角色。

  这两个组件结合在一起,使开发人员能够在数据流而不是控制流方面利用并行性,并通过更高效地使用并行资源来改善对延迟的容忍度。
消息传递框架

  AAL 的第一个重要组件是消息传递框架,该框架是协助开发数据流网络以便将工作管道化的一组构造。 将工作管道化是数据流模型的基本部分,因为它允许将工作分解为多个独立的阶段,只要数据就绪便可对流数据进行并行处理。 当一个阶段的数据处理结束时,该阶段可将数据传递到下一阶段,同时第一个阶段寻找要处理的新数据。

  我们以设置传出消息格式并审查消息中是否存在不当内容的电子邮件应用程序为例。 这种类型操作的代码显示如下:

          std::foreach(reader.begin(); reader.end();
  [](const string& word) {
    auto w1 = censor(word);
    auto w2 = format(w1);
    writer.write_word(w2);
  });
       

  对于电子邮件中的每个词,该应用程序都需要检查它是否存在于审查词的字典中,如果存在则予以替换。 然后,代码根据一组指导原则设置每个词的格式。

  这种方案中存在大量固有的并行性。 但是,传统并行技术还不能满足要求。 例如,一种简单的方法是对文本中的字符串使用 parallel_for_each 算法,审查这些字符串并设置格式。

  这种解决方案的第一个主要阻碍是必须读取整个文件,以便迭代器能够正确地划分工作。
强制读取整个文件会导致进程受到 I/O 的限制,并且会降低并行效率。 当然,您可以使用智能迭代器将词的处理与读取输入的操作重叠进行。

  传统并行方法的第二个主要问题是排序。 显然,对于电子邮件来说,对文本的并行处理必须保持文本顺序,否则会完全无法理解邮件的含义。 为了保持文本顺序,parallel_for_each 技术会产生同步和缓冲方面的大量开销,而这一过程可由 AAL 自动处理。

  通过采用管道技术处理邮件,您可以避免上述两个问题,同时还能利用并行能力。 请看一下图 2,其中创建了一个简单管道。 在此示例中,应用程序的主要任务(审查和设置格式)被分为两个阶段。 第一个阶段接收字符串并在审查词的字典中查找该字符串。 如果找到匹配项,审查块会使用字典中的另一个词替换该字符串。 否则,它会输出已输入的同一封邮件。 同样,在第二个阶段中,格式设置块接收每个词并将其恰当地设置为特定样式。

异步代理: 使用异步代理库进行基于角色的编程

  图 2 电子邮件处理管道

  此示例可在以下几个方面从数据流方法获益。 首先,由于它不需要在处理前读取整封邮件,邮件中的字符串可以通过审查和设置格式阶段立即开始流处理。 其次,管道处理允许一个字符串由设置格式块进行处理,同时下一个字符串由审查块进行处理。 最后,由于字符串的处理顺序是它们在原文中出现的顺序,因此不需要执行额外的同步。
消息块

  消息块接收、处理、存储和传播消息。 消息块有三种形式:源、目标和传播器。 源只能传播消息,而目标能够接收、存储和处理消息。 大多数块都是传播器,既是源又是目标。 换句话说,它们能够接收、存储和处理消息,也可以转而将这些消息发送出去。

  AAL 包含一组消息块基元,能够满足开发人员的大部分使用需求。 图 3 简要概述了 AAL 中包括的所有消息块。 不过该模型仍然是开放式的,因此,如果您的应用程序需要具有特定行为的消息块,可以自己编写可与所有预定义块交互的自定义块。 每个块都有各自处理、存储和传播消息的独有特征。

  图 3 AAL 消息块

消息块