让我们进入 NET Framework 此版本 NET Framework 扩充了大量功能显着降低了开发人员在应用程序中表示并行性的难度并提高了并行执行的效率 这远远超出了并行循环的范畴尽管如此我们仍将从这里开始
SystemThreading 命名空间在 NET Framework 中通过一个新的子命名空间得到了增强SystemThreadingTasks 此命名空间包含一个新类型 Parallel该类型公开了丰富的静态方法用以实现并行循环和结构化派生联结模式 作为其用法的示例请考虑前面的 for 循环
借助 Parallel 类可以如下所示轻松实现并行化
在这里开发人员仍负责确保循环的每个迭代实际上都是独立的但除此之外ParallelFor 构造会处理此循环的并行化的所有方面 该构造跨计算中涉及的所有基础线程动态地对输入范围进行分区同时仍尽可能减小分区的开销使其接近于静态分区实现的开销 该构造动态地增加和减少计算中涉及的线程数以便发现最适合给定工作负载的最佳线程数(与惯常认知不同的是最佳线程数并不总是等于硬件线程数) 该构造还提供前面演示的简单实现中不存在的异常处理功能等等 最重要的是该构造使开发人员不必在线程的较低级别操作系统抽象层考虑并行性并且无需为工作负载分区卸载到多个核心以及高效地联接结果而一次又一次地编写谨小慎微的解决方案 如此开发人员便可集中时间处理更重要的工作使开发人员的工作更富收益的业务逻辑
ParallelFor 还为需要更加精细地控制循环操作的开发人员提供了便利工具 通过为 For 方法提供的一个选项包开发人员可以控制循环运行所在的基础计划程序要使用的最大并行度以及循环外部实体用于请求在循环方便时尽早正常终止的取消标记
此自定义功能突出了 NET Framework 中此并行化工作的一个目标在不使编程复杂化的情况下显着降低开发人员利用并行性的难度同时为更加高级的开发人员提供了对处理和执行进行微调所需的工具 出于此目的还支持其他调整 ParallelFor 的其他重载使开发人员可以尽早中断循环
还有其他重载使开发人员能够使状态流过最终在同一基础线程上运行的迭代从而显着提高算法实现(如缩减)的效率例如
Parallel 类不仅为整数范围提供支持还为任意 IEnumerable 源(可枚举序列的 NET Framework 表示形式)提供支持代码可以对枚举器连续地调用 MoveNext以便检索下一个 Current 值 通过这种使用任意可枚举内容的能力可实现任意数据集的并行处理而无论数据在内存中的表示形式如何数据源甚至可以根据需要具体化并在 MoveNext 调用到达源数据的尚未具体化部分时分页 与 ParallelFor 一样ParallelForEach 也采用许多自定义功能但提供比 ParallelFor 更大的控制能力 例如ForEach 使开发人员可以自定义对输入数据集进行分区的方式 这通过一组侧重于分区的抽象类完成这些抽象类使并行化构造可以请求固定或可变数量的分区从而允许分区程序将这些分区抽象分派给输入数据集并根据需要以静态或动态方式将数据分配给这些分区
ParallelFor 和 ParallelForEach 在 Parallel 类上补充提供了一个 Invoke 方法该方法接受任意数量的待调用操作并可实现基础系统可以支持的最大并行度 通过此经典的派生联结构造可以轻松地并行化递归的分割解决算法如常用的 QuickSort 示例
尽管有了很大进步但是 Parallel 类只是可用功能的一小部分 NET Framework 中实现的更重大的并行化进步之一是引入了并行 LINQ人们将其亲切地称为 PLINQ(发音为Peelink) LINQ(即语言集成查询)是在 NET Framework 版本 中引入的 LINQ 实际包含两方面内容对一组公开为数据集操作方法的运算符的描述以及 C# 和 Visual Basic 中用于直接在语言中表示这些查询的上下文关键字 LINQ 中包含的许多运算符都基于数据库社区多年以来所了解的等效运算包括 SelectSelectManyWhereJoinGroupBy 以及大约 个其他运算 NET Framework 标准查询运算符 API 为这些方法定义了模式但是未定义这些运算应针对的确切数据集也未确切定义应如何实现这些运算 各种LINQ 提供程序随后为许多不同数据源和目标环境(内存中集合SQL 数据库对象/关系映射系统HPC Server 计算群集临时和流数据源等等)实现此模式 最常用的提供程序之一名为 LINQ to Objects它提供以 IEnumerable 为基础实现的全套 LINQ 运算符 这样便可在 C# 和 Visual Basic 中实现查询如下面的代码段所示该代码段从文件逐行读取所有数据从而仅筛选出包含secret一词的行并对这些行进行加密 最终结果是字节数组构成的可枚举内容 对于需要大量计算的查询甚至只对于涉及大量长延迟 I/O 的查询PLINQ 提供自动并行化功能从而实现利用端到端并行算法的完整 LINQ 运算符集 因此开发人员只需为数据源附加AsParallel()即可并行化前面的查询
与 Parallel 类一样此模型也选择强迫开发人员评估并行运行计算的后果 但是一旦做出并行选择系统便会处理实际并行化分区线程限制等较低级别的细节 此外与 Parallel 一样这些 PLINQ 查询也可通过各种方式进行自定义 开发人员可以控制如何实现分区实际使用的并行度同步与延迟之间的权衡等
这些用于循环和查询的编程模型功能强大级别更高它们构建于同样强大但是级别较低的一组基于任务的 API 之上以 SystemThreadingTasks 命名空间中的 Task 和 Task 类型为中心 实际上并行循环和查询引擎属于任务生成器依靠基础任务基础结构将表示的并行性映射到基础系统中提供的资源 在其核心Task 是工作单元(或者更宽泛地说是异步单元即可能生成并在以后通过各种方式进行联接的工作项)的表示形式 Task 提供 WaitWaitAll 和 WaitAny 方法这些方法允许同步阻止向前推进直至目标任务完成或直至向这些方法的重载提供的其他约束得到满足(例如超时或取消标记) Task 通过其 IsCompleted 属性支持轮询任务是否完成更宽泛地说通过其 Status 属性支持轮询其生命周期处理中的更改 可能最重要的是它提供 ContinueWithContinueWhenAll 和 ContinueWhenAny 方法通过这些方法可以创建仅当完成一组特定先行任务时才安排的任务 通过此续接支持可以轻松实现许多方案从而可以在计算之间表示依赖关系以便系统可以基于这些依赖关系的满足情况安排工作
通过从 Task 派生的 Task 类可以从完成的操作传出结果从而向 NET Framework 提供核心未来实现
在所有这些模型(循环查询和任务等)之下NET Framework 使用工作窃取方法提供对专门工作负载的更高效处理并且在默认情况下它使用爬山试探法随时间推移改变使用的线程数以便找到最佳处理级别 试探法也内置在这些组件的各个部分之中以便在系统认为任何并行化尝试都会导致慢于顺序结果时间时自动回退到顺序处理不过与前面讨论的其他默认设置一样这些试探法也可被覆盖
Task 无需仅表示计算密集型操作 它还可以用于表示任意异步操作 请考虑 NET Framework SystemIOStream 类该类提供 Read 方法用于从流中提取数据
此 Read 操作是同步阻塞操作这样便不会将进行 Read 调用的线程用于其他工作直至基于 I/O 的 Read 操作完成 为了实现更好的可伸缩性Stream 类以两个方法的形式为 Read 方法提供了异步对应项BeginRead 和 EndRead 从 NET Framework 诞生之初这些方法便遵循其中提供的模式该模式称为 APM(即异步编程模型) 下面是前面代码示例的异步版本
但是这种方法会导致可组合性较差 TaskCompletionSource 类型通过使这类异步读取操作可以公开为任务解决了此问题
这样便允许组合多个异步操作正如计算密集型示例中一样 下面的示例同时从所有源数据流读取数据仅当所有操作完成后才写出到控制台
除了用于启动并行化和并发处理的机制之外NET Framework 还为任务与线程之间的进一步协调工作提供了基元 这包括一组线程安全且可伸缩的集合类型这些类型大体消除了开发人员手动同步对共享集合的访问的需要 ConcurrentQueue 提供一个线程安全无锁定先进先出的集合该集合可以由任意数量的生产者和任意数量的使用者同时使用 此外它还支持并发枚举器的快照语义以便代码即使在其他线程操作实例时也可以检查队列的状态 ConcurrentStack 也类似只是它提供后进先出语义 ConcurrentDictionary 使用无锁定和精细锁定方法提供线程安全的字典该字典也支持任意数量的并发读取器写入器和枚举器 它还提供几个多步骤操作(如 GetOrAdd 和 AddOrUpdate)的原子实现 另一种类型 ConcurrentBag 提供使用工作窃取队列的无序集合
NET Framework 不会停止开发集合类型 Lazy 通过采用可配置的方法实现线程安全提供变量的延迟初始化 ThreadLocal 提供每线程每实例数据这些数据也可以在第一次访问时延迟初始化 Barrier 类型实现阶段化操作以便可以通过保持同步的算法进行多个任务或线程 该列表在不断扩充并且所有内容都源自一个指导原则开发人员应无需关注其算法并行化的低级及初级方面而是让 NET Framework 为其处理功能机制和效率细节