并行 .net 应用程序的过去、现在和未来

xiaoxiao2021-03-01  11

并行 .net 应用程序的过去、现在和未来 2011年08月31日   以往,开发人员在尝试实现响应及时的客户端应用程序、并行算法和可伸缩服务器时,一直都采用直接线程操作。 然而,这类方法也一直为开发人员制造着麻烦,比如死锁、活锁、锁保护、两步舞曲、争用条件、过度订阅和应用程序中的许多其他缺陷。 从诞生之初,Microsoft .NET Framework 便提供了大量较低级别的工具,用于构建并发应用程序,包括专为这项工作提供的整个命名空间:System.Threading。 借助 .NET Framework 3.5 核心程序集在此命名空间中提供的大约 50 种类型(包括 Thread、ThreadPool、Timer、Monitor、ManualResetEvent、ReaderWriterLock 和 Interlocked 等类型),人们不应责怪 .NET Framework 的线程支持太少。 不过,在我看来,.NET Framework 的以往版本对开发人员提供的真正支持仍有欠缺,不足以让他们在任何位置都能成功构建可伸缩且高度并行化的应用程序。 我十分欣慰和兴奋地告诉大家,这个问题在 .NET Framework 4 中得到了纠正,并且对未来 .NET Framework 版本会继续有大量投资。   有些人可能会质疑托管语言中的丰富子系统对于编写并行代码的价值。 毕竟,并行性和并发性关乎性能,而注重性能的开发人员应寻求本机语言,因为这些语言可提供对硬件的充分访问以及对每个位调整、缓存行操作和联锁操作的完全控制… 对吧? 如果情况确实如此,我会为我们行业的现状感到担忧。 托管语言(如 C#、Visual Basic 和 F#)的存在是为了向所有开发人员 ― 无论是无名小卒还是超级英雄 ― 提供一个安全且富有成效的环境,用于快速开发强大而高效的代码。 开发人员有成千上万的预生成库类可供使用,还有包含我们所期待的所有先进服务的成熟语言,并且仍设法在几乎最繁重的数字处理和浮点密集型工作负载下实现卓越的性能。 所有这些意味着,托管语言及其关联框架针对构建高性能并发应用程序提供了深层支持,使得使用最新硬件的开发人员也可以从中受益。   我一直认为模式学习是一种很好的学习方法,因此对于当前的主题,我们最好也通过了解一种模式来开始我们的探索。 无论是“进退两难的并行”还是“令人愉快的并行”模式,最常需要的一种“派生-联结”构造都是并行循环,该循环旨在并行处理循环中的每个独立迭代。 了解如何使用前面提到的较低级别基元进行这类处理颇具指导意义,为此我们将演练一下使用 C# 实现的简单并行循环的基本实现。 考虑下面的典型 for 循环:   for (int i=0; i不能由所用线程数均匀分割,导致最后一个线程承受溢出的情况)。 然而,最糟糕的可能是开发人员可能一开始便被强迫编写此代码。 我们尝试并行化的每个算法都需要类似的代码(十分脆弱的代码)。   当我们认识到并行循环只是并行程序中存在的众多模式中的一个模式时,会进一步放大通过上面代码举例说明的问题。 强迫开发人员以此低级别编码表示所有这类并行模式不会形成良好的编程模型,不会让世界上需要利用高度并行硬件的众多开发人员取得成功。   并行编程的现在让我们进入 .NET Framework 4。 此版本 .NET Framework 扩充了大量功能,显著降低了开发人员在应用程序中表示并行性的难度,并提高了并行执行的效率。 这远远超出了并行循环的范畴,尽管如此,我们仍将从这里开始。   System.Threading 命名空间在 .NET Framework 4 中通过一个新的子命名空间得到了增强:System.Threading.Tasks。 此命名空间包含一个新类型 Parallel,该类型公开了丰富的静态方法,用以实现并行循环和结构化“派生-联结”模式。 作为其用法的示例,请考虑前面的 for 循环:   for (int i=0; i { ... // Process i here});   在这里,开发人员仍负责确保循环的每个迭代实际上都是独立的,但除此之外,Parallel.For 构造会处理此循环的并行化的所有方面。 该构造跨计算中涉及的所有基础线程动态地对输入范围进行分区,同时仍尽可能减小分区的开销,使其接近于静态分区实现的开销。 该构造动态地增加和减少计算中涉及的线程数,以便发现最适合给定工作负载的最佳线程数(与惯常认知不同的是,最佳线程数并不总是等于硬件线程数)。 该构造还提供前面演示的简单实现中不存在的异常处理功能,等等。 最重要的是,该构造使开发人员不必在线程的较低级别操作系统抽象层考虑并行性,并且无需为工作负载分区、卸载到多个核心以及高效地联接结果而一次又一次地编写谨小慎微的解决方案。 如此,开发人员便可集中时间处理更重要的工作:使开发人员的工作更富收益的业务逻辑。   Parallel.For 还为需要更加精细地控制循环操作的开发人员提供了便利工具。 通过为 For 方法提供的一个选项包,开发人员可以控制循环运行所在的基础计划程序、要使用的最大并行度,以及循环外部实体用于请求在循环方便时尽早正常终止的取消标记:   var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };Parallel.For(0, N, options, i=> { ... // Process i here});   此自定义功能突出了 .NET Framework 中此并行化工作的一个目标:在不使编程复杂化的情况下显著降低开发人员利用并行性的难度,同时为更加高级的开发人员提供了对处理和执行进行微调所需的工具。 出于此目的,还支持其他调整。 Parallel.For 的其他重载使开发人员可以尽早中断循环:   Parallel.For(0, N, (i,loop) => { ... // Process i here if (SomeCondition()) loop.Break();});   还有其他重载使开发人员能够使状态流过最终在同一基础线程上运行的迭代,从而显著提高算法实现(如缩减)的效率,例如:   static int SumComputations(int [] inputs, Func computeFunc) { int total = 0; Parallel.For(0, inputs.Length, () => 0, (i,loop,partial)=> { return partial + computeFunc(inputs); }, partial => Interlocked.Add(ref total, partial));}   Parallel 类不仅为整数范围提供支持,还为任意 IEnumerable 源(可枚举序列的 .NET Framework 表示形式)提供支持:代码可以对枚举器连续地调用 MoveNext,以便检索下一个 Current 值。 通过这种使用任意可枚举内容的能力可实现任意数据集的并行处理,而无论数据在内存中的表示形式如何;数据源甚至可以根据需要具体化,并在 MoveNext 调用到达源数据的尚未具体化部分时分页:   IEnumerable lines = File.ReadLines("data.txt");Parallel.ForEach(lines, line => { ... // Process line here});   与 Parallel.For 一样,Parallel.ForEach 也采用许多自定义功能,但提供比 Parallel.For 更大的控制能力。 例如,ForEach 使开发人员可以自定义对输入数据集进行分区的方式。 这通过一组侧重于分区的抽象类完成,这些抽象类使并行化构造可以请求固定或可变数量的分区,从而允许分区程序将这些分区抽象分派给输入数据集,并根据需要以静态或动态方式将数据分配给这些分区:   Graph graph = ...;Partitioner data = new GraphPartitioner(graph);Parallel.ForEach(data, vertex => { ... // Process vertex here});   Parallel.For 和 Parallel.ForEach 在 Parallel 类上补充提供了一个 Invoke 方法,该方法接受任意数量的待调用操作,并可实现基础系统可以支持的最大并行度。 通过此经典的“派生-联结”构造可以轻松地并行化递归的“分割-解决”算法,如常用的 QuickSort 示例:   static void QuickSort(T [] data, int lower, int upper) { if (upper 相关资源:敏捷开发V1.0.pptx
转载请注明原文地址: https://www.6miu.com/read-3200317.html

最新回复(0)