在多核处理器上运行查询

ycJ_154 2007-09-29 10:25:48
多核处理器的时代已经来临。多核处理器曾经主要普遍应用于服务器和台式计算机,现在正转向移动电话和 PDA,这为节省耗电量带来了极大的好处。面对市场上多核处理器平台的日益增多,并行语言集成查询 (PLINQ) 为利用并行硬件(包括传统的多处理器计算机和新一代的多核处理器)的优势提供了一种简单的方法。

PLINQ 是一个查询执行引擎,它接受任何“LINQ 到对象”或“LINQ 到 XML”的查询,并在多处理器或多核可用时自动将其用于执行。编程模型中的变化微乎其微,这意味着您无需成为并发专家便可使用它。事实上,除非您真的想要深入了解所有相关工作原理,否则线程和锁的问题甚至都不会接触到。PLINQ 是 Parallel FX 的关键组件,后者则是 Microsoft® .NET Framework 中的下一代并发支持。

在确保未来并行微处理器体系结构的软件可伸缩性方面,使用 PLINQ 之类的技术将变得日益重要。如果现在就在整个应用程序中的首选位置采用 LINQ(例如在可表达为查询的数据或计算密集型运算之处),即可确保在 PLINQ 成为可用且运行您应用程序的计算机从 2 处理器升级到 4 处理器,再升级到 32 处理器甚至更多处理器时,您程序的片段会继续更好地执行。而且即使您只在单处理器计算机上运行该代码,PLINQ 的开销通常也很小,您甚至不会察觉出差异。除此之外,PLINQ 的数据并行特性可确保您的程序会随着数据集规模的增加而继续扩展。

在本文中,我们将回顾 PLINQ 技术的目标、该技术与更多种 .NET Framework 及其他并发产品的接合点,以及 LINQ 开发人员对该技术的看法。最后,我们将结合一些示例情景进行总结,从中可以看出 PLINQ 已经显现出巨大的价值。

请注意,包含 PLINQ 的 Parallel FX 库目前仍处于开发阶段,但是社区技术预览版 (CTP) 将在 2007 年秋季的 MSDN® 中首次推出。请阅读博客 blogs.msdn.com/somasegar 以了解详细信息。


从 LINQ 到 PLINQ

人们初次听到 PLINQ 项目时,通常会问:为什么要并行化 LINQ?简单的答案是,LINQ 具有表达计算的一次处理一个集合 (set-at-a-time) 的编程模型,侧重的是指定要实现的目标,而不是实现方式。如果没有 LINQ,实现部分通常会通过使用循环和中间数据结构来表达,但是在编码这么多特定信息的情况下,编译器和运行时就无法轻松地并行化。另一方面,LINQ 的声明性特性使 PLINQ 之类的智能化实现可以更灵活地利用并行化来取得相同的结果。

接下来必然会问到这样的问题:如果您打算查询足够的数据以证明 PLINQ 的意义,那为什么只用一个数据库?我们将以一个反问句来回答这个问题:如果不使用 LINQ,您会编写什么代码?您可能会做同样多的数据和计算密集型工作,但可能会陷入一系列杂乱无章的循环、函数调用等等。所以,这个问题说明所有程序都应驻留在数据库中,但是我相信没有多少人会同意这个观点。

有了 PLINQ,您不必将整个数据库服务器处理逻辑移到客户端的内存中“LINQ 到对象”查询。相反,PLINQ 提供了一种增量式方法,利用现有解决方案到现有问题的并行性。如果您的问题数据密集性足够高,而且传统上会要求您寻求数据库解决方案,那么您应仍然寻求这样的解决方案。PLINQ 几乎没有改变这一点。但是,如果有若干想要一起查询的数据源(可能是异类、跨平台数据库、XML 文件等),PLINQ 可在数据位于客户端后接管并并行化它们。

人们接着自然会问:“LINQ 到对象”本身为什么没有并行运行查询?并行性是完全隐藏的,开发人员无需更改任何一行代码便能获得并行性的好处。遗憾的是,当从 LINQ 迁移到 PLINQ 时可能会出现一些细微的限制,使我们无法实现此愿景,至少对 PLINQ 1.0 来说是这样的。本文稍后将更详细地讨论这些问题。


PLINQ 编程模型

使用 PLINQ 几乎就与使用“LINQ 到对象”和“LINQ 到 XML”一模一样。您可以使用 C# 3.0 和 Visual Basic® 9.0 查询理解语法或 System.Linq.Enumerable 类提供的任何运算符,包括 OrderBy、Join、Select、Where 等。PLINQ 支持所有 LINQ 运算符,而不仅仅支持那些语言理解中的运算符。此外,除了使用 System.Xml.Linq API 加载的 XML 文档外,您还可以查询任何内存中集合,如 T[], List<T>,或任何其他类型的 IEnumerable<T>。如果已经拥有大量的 LINQ 查询,则 PLINQ 有能力来运行它们。

“LINQ 到 SQL”和“LINQ 到实体”查询将仍然由各自的数据库和查询提供程序执行,因此,PLINQ 并不提供并行化这些查询的方法。如果您希望在内存中处理这些查询的结果(包括联接许多异类查询的输出),那么 PLINQ 的用处将很大。

除了用您习惯的同一方法编写 LINQ 查询之外,使用 PLINQ 还需要两个额外的步骤:


在编译期间引用 System.Concurrency.dll 程序集。
通过调用 System.Linq.ParallelEnumerable.AsParallel 扩展方法,将数据源封装到 IParallelEnumerable<T> 中。

在步骤 2 中调用 AsParallel 扩展方法可确保 C# 或 Visual Basic 编译器绑定到标准查询运算符的 System.Linq.ParallelEnumerable 版本,而非 System.Linq.Enumerable。这使得 PLINQ 能够取得控制权并并行执行查询。AsParallel 定义为接受任何 IEnumerable<T>:


public static class System.Linq.ParallelEnumerable {
public static IParallelEnumerable<T> AsParallel<T>(
this IEnumerable<T> source);
... the other standard query operators ...
}


IParallelEnumerable<T> 从 IEnumerable<T> 派生而来,但并无多大补充,它存在的目的只是为了帮助绑定到 PLINQ 的 ParallelEnumerable 查询提供程序,以利用 C# 3.0 和 Visual Basic .NET 9.0 中的新扩展方法支持。该接口派生自 IEnumerable<T>,以便您仍然可以对实例执行 foreach,并将它们传递给其他接受 IEnumerable<T> 的 API。在 ParallelEnumerable 上定义的标准查询运算符是 Enumerable 上运算符的镜像,其唯一差别在于每个运算符都取 IParallelEnumerable<T> 而不是 IEnumerable<T> 来作为其扩展源参数,然后返回 IParallelEnumerable<T> 而非 IEnumerable<T>(只返回简单类型的聚合除外),当然,还会在内部采用并行性以进行查询评估。

以 C# 中定义的一个简单 LINQ 查询为例:


IEnumerable<T> data = ...;
var q = data.Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
foreach (var e in q) a(e);


此例中使用 PLINQ 所要求的全部条件是添加一个对数据的 AsParallel 调用:


IEnumerable<T> data = ...;
var q = data.AsParallel().Where(x => p(x)).Orderby(x => k(x)).Select(x => f(x));
foreach (var e in q) a(e);


使用 C# 查询理解语法可以更加简洁地编写此代码,此时的 PLINQ 版本如下所示:


IEnumerable<T> data = ...;
var q = from x in data.AsParallel() where p(x) orderby k(x) select f(x);
foreach (var e in q) a(e);


一旦您进行此更改后,PLINQ 将使用典型的数据并行评估技术,在所有可用处理器上透明地执行 Where、OrderBy 和 Select。PLINQ 就像 LINQ 一样采用延迟执行,这表示在对查询执行 foreach、直接调用 GetEnumerator 或通过 ToList 或 ToDictionary 之类的某些其他 API 将结果强制列入某个列表之后,该查询才会开始运行。当查询执行时,PLINQ 将通过对多线程的隐藏使用,将查询的各个部分安排在可用处理器上运行。您甚至不必了解这一切是如何完成的;您只用看到更高的性能和对可用处理器利用率的提高就可以了。

虽然不是很明显,上文所示的普通“LINQ 到对象”和 PLINQ 查询之间的 q 推断类型是不同的。在第一个示例中,q 被类型化为 IEnumerable<U>,其中 U 是 f 方法传递给 Select 运算符的任何类型。但在第二个示例中,q 被类型化为 IParallelEnumerable<U>。通常这无关紧要:例如,如果您已经将 q 的类型显式声明为 IEnumerable<U>,那么对 AsParallel 的更改将仍然有效,因为 IParallelEnumerable<U> 派生自 IEnumerable<U>。但是,这确实表示 q 的任何后续使用会被当作 IParallelEnumerable<U>。例如,如果您以后查询它,PLINQ 会被选为查询提供程序。

请注意,有些 LINQ 运算符是二元的——它们取两个 IEnumerable<T> 作为输入。Join 就是此类运算符的一个最好的例子。在这些情况下,最左边数据源的类型决定了是使用 LINQ 还是 PLINQ。因此,您只需在第一个数据源上调用 AsParallel 便能使查询并行运行。


IEnumerable<T> leftData = ..., rightData = ...;
var q = from x in leftData.AsParallel()
join y in rightData on x.a == y.b
select f(x, y);


所有这些讨论都假设您使用扩展方法编写查询。但是,如果您已经选择直接调用 Enumerable 类上的方法,那么将需要多做一点工作才能迁移到 PLINQ。除了调用 AsParallel 外,还必须引用 ParallelEnumerable 类型。例如,假设上述查询是通过直接调用 Enumerable 来编写的:


IEnumerable<T> data = …;
var q = Enumerable.Select(
Enumerable.OrderBy(
Enumerable.Where(data, (x) => p(x)),
(x) => k(x)),
(x) => f(x));
foreach (var e in q) a(e);


要使用 PLINQ,必须按如下方式重新编写该查询:


IEnumerable<T> data = …;
var q = ParallelEnumerable.Select(
ParallelEnumerable.OrderBy(
ParallelEnumerable.Where(data.AsParallel(), (x) =>    p(x)),
(x) => k(x)),
(x) => f(x));
foreach (var e in q) a(e);


显而易见,使用理解和扩展方法是更为方便的一种查询编写方法,而且还有一个额外的好处,那就是大大简化了迁移到 PLINQ 的过程。

就是这些! 用 PLINQ 代替“LINQ 到对象”就要求这些变动。由于“LINQ 到 XML”将 XML 文档作为 IEnumerable<T> 数据结构公开,因此上述所有内容也都适用于查询 XML 内容。


处理查询输出

前面已经提到,由于延迟评估,在开始处理查询输出之前不会引入并行性。如果您熟悉 IEnumerable<T>,就该知道这相当于调用 GetEnumerator 方法。处理 PLINQ 查询的基本方法有三种,各自都会产生一种稍有差别的并行性模型。

第一种方法是管道式处理。在这种情况下,负责枚举的线程与专门运行查询的线程是分开的。PLINQ 将使用许多线程以执行查询,但是会将并行度减少一个,以使枚举线程不受干扰。例如,如果有八个处理器可用,其中七个将运行 PLINQ 查询,剩下的一个处理器便可在元素可用时对 PLINQ 查询的输出运行 foreach 循环。这样的好处是允许对输出进行更多增量处理,从而减少存放结果所需的内存要求;但是,如果有许多生产者线程而只有一个使用者线程,这通常会导致工作分配不平衡,结果是处理器效率低下。

第二种模型是准动态 (stop-and-go) 处理。在这种模型下,启动枚举的线程会联接所有其他线程以执行查询。一旦所有线程已经生成一组完整输出,该线程就会继续枚举输出。这样做的好处是所有处理能力都会集中在尽快产生输出上。此外,它的效率也比管道式处理稍有提高,因为实现过程中的增量式同步系统开销减少了:对于这些方面,PLINQ 可以更加智能化,因为它确切地知道输出数据的目的地以及访问它的方式。如果使用者和生产者之间的工作分配不平衡,这通常是一个更恰当的消耗方法。

最后一种方法是反转枚举。该方法会为并行运行的 PLINQ 提供一个 lambda 函数,为输出中的每个元素提供一次。这是最高效的一种机制,因为公开给 PLINQ 的并行性更多,而且该实现也避免了高成本的运算,例如合并多个线程的输出。但是它也存在一些缺陷:您不能简单地使用 foreach 循环,而必须使用特殊的 ForAll API,必须特别注意 lambda 不依赖共享状态。否则,引入并行性将会导致查询出错,并可能导致不可预料的崩溃或数据损坏。但是,如果您可以就此 API 表达您的问题,这始终是一个首选方法。

采用这三种模型中的哪一种取决于您将如何处理查询结果。默认的模型是第一种,即管道式处理。一旦对产生的查询枚举器调用 MoveNext 后,一组额外的工作线程会立即执行查询,并且此次调用以及所有后续 MoveNext 调用的结果(当它们可用时)都会返回。如果调用了 MoveNext,但是查询生产者线程没有任何输出,则调用线程将阻塞,直至某个元素可用。如果只用了 foreach 来处理 PLINQ 查询的输出,您将获得如下所示的结果:


var q = ... some query ...;
foreach (var e in q) {
a(e); // this runs in parallel with the execution of 'q'
}


IParallelEnumerable<T> 接口实际上提供了 GetEnumerator 的重载,该函数接受一个 bool 参数(称为管道式),并允许您选择准动态处理来代替(true 表示管道式,false 表示准动态)。第一次后续调用 MoveNext 时,将执行整个查询,而且仅在所有输出可用时调用才会返回。后续调用 MoveNext 将只枚举包含以下输出的缓冲区:


var q = ... some query ...;
using (var e = q.GetEnumerator(false)) {
while (e.MoveNext()) {
// after the 1st call, the query is finished executing
// we then just enumerate the results from an in-memory list
a(e.Current);
}
}


在几种特殊情况下,默认使用准动态处理:如果您使用 ToArray 或 ToList 方法,这些运算符将在内部强制执行准动态操作。如果在查询中有排序,则将转而使用准动态操作,因为将排序输出管道化纯属浪费。排序表现出极度滞后性(因为它通常需要对整个输入排序后才能生成单一输出元素),因此,PLINQ 更愿意将所有处理能力用于尽快完成排序。

要使用反转枚举,必须使用一个不同的 PLINQ 特定 API:


public static class System.Linq.ParallelEnumerable {
public static void ForAll<T>(
this IParallelEnumerable<T> source, Action<T> action);
... the other standard query operators ...
}


使用 ForAll API 与使用 foreach 循环非常相似,正如您所看到的:


var q = … some query …;
q.ForAll(e => a(e));
...全文
109 2 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
2 条回复
切换为时间正序
请发表友善的回复…
发表回复
ycJ_154 2007-09-29
  • 打赏
  • 举报
回复
PLINQ 依赖于所谓的统计纯度:大多数 LINQ 查询在多数时候都不会改变数据结构或执行不纯的操作。也就是说,大多数 LINQ 查询都是严格意义上的功能性操作;它们接受某些数据作为输入、执行某些计算,并创建完全独立的数据副本,进行修改以作为输出。但是编译器或运行库并不以任何方式强制实施此类最佳实践用法。如果人们已经编写了突破此常见使用模式的查询,那么转移到 PLINQ 可能会变得更加复杂。

比方说,考虑以下的 LINQ 查询:


var q = from x in data where (x.f-- > 0) select x;


请注意,where 子句中的谓词实际上修改了某个对象的一个字段。这意味着若不公开争用状况和并发 bug,并行运行可能是不安全的。在默认情况下,您必须假设此类查询是不安全的,不可以用 PLINQ 运行。但是,它何时才会真正安全呢?只有当谓词在多个针对同一个对象的线程上运行时才会发生争用情况,而这种情况只有在数据包含重复对象时才会发生。因此,如果数据是一个集合,那就完全没有问题。

还有其他一些肯定不安全的情况,例如在使用静态变量的地方:


class C { internal static int s_cCounter; }
// elsewhere…
var q = from x in data where (x.f == C.s_cCounter++) select x;


如果并行运行此查询,您很可能会非常失望。最终的结果可能是含重复 f 字段值的许多对象,这种情况只在出现争用状况时才会发生。注意:为了解决这个问题,您最初的反应和方法可能是用 Interlocked.Increment(ref C.s_cCounter) 代替 C.s_cCounter++。尽管通过此方法会找到正确的解决方案,但是任何类型的同步都将大大降低查询的可伸缩性。作为首要原则,应努力消除查询中所有对变动性和共享状态的依赖性。

Windows® 平台是以线程为中心的,而且许多系统状态最后都可能附加到本身运行某些代码片段的线程上。可以采取的形式包括线程本地存储、诸如模拟的安全信息、COM 单元(特别是单线程单元),以及 Windows 窗体和 Windows Presentation Foundation 之类的 GUI 框架。从 LINQ 迁移至 PLINQ 时,所有这些都可能引来麻烦。

在 LINQ 模型中,查询中的所有代码都在启动该查询的同一个线程上运行,而在 PLINQ 中,查询中的代码分布在许多线程上。遗憾的是,当您已经对线程关联存在依赖关系时,这并不总是很明显,因为 .NET Framework 中的许多服务在内部都依赖这种关联,而且这种依赖是固有和透明的。我能给出的最佳建议是警惕上述并发危险源,并在您的查询中尽量避免它们。


开始执行 PLINQ

PLINQ 使 LINQ 开发人员能够利用并行硬件的优势(包括实现更佳性能和构建本质上更具可伸缩性的软件)而无需理解并发或数据并行性。编程模型很简单,使更多的开发人员能够利用更多的并行硬件。“PLINQ 资源”侧栏中列出一些参考资料,可供了解更多信息。

对于那些已经使用“LINQ 到对象”、“LINQ 到 XML”或 LINQ 将异类数据源结合在一起的用户,让现有查询使用 PLINQ 是一个简单的过程。若要进一步了解此技术的可能用法,请参阅“有趣的情景”侧栏。对于那些尚未开始采用 LINQ 的用户而言,并行加速的益处是您考虑使用它的另一个原因。即使目前 PLINQ 尚未推出,在程序中使用 LINQ 也是对未来的一项投资。
http://msdn.microsoft.com/msdnmag/issues/07/10/PLINQ/default.aspx?loc=zh/&print=true
ycJ_154 2007-09-29
  • 打赏
  • 举报
回复
并发异常
有趣的情景
在阅读本文时, 您可能已经开始设想在自己的应用程序中如何采用 PLINQ 的一些方法。也许您目前已经在使用 LINQ,并且想要提高多处理器或多核计算机上的应用程序的可伸缩性。PLINQ 无疑可以让现有的程序运行得更快,除此之外,它还可以让您执行更多的计算工作,在相同的时间内操作更多的数据,同时能够以更快的速率处理数据流。在实现这些功能的过程中,新的 PLINQ 技术可能开启了您以前无法尝试的新应用领域。

让我们来看一些情景说明,在这些情景中,多核和 PLINQ 开启了新的领域。假设某个在录音室工作的音乐创作人想对乐器原声应用一系列效果,以制作更具美感、更让人愉悦的商业品质主音轨。为他提供混音软件的公司可以使用 PLINQ 来应用这些效果。这些效果通常都由大量数据流(原始音乐)上的过滤器和投影组成。PLINQ 可以大大加速制作时间并能利用更强大的硬件(在它出现时)。此方法甚至可以实现近乎实时的音乐转换,而不用进行完整的后期制作处理。

再来假设一个类似的情景,一名外汇交易员正在为了获利而寻找套利条件(市场无效率)。此类变化非常细微,并且会随着市场不断达到平衡而消失,所以需要快速交易。利用 PLINQ 提供的并行性来查询股票交易信息即可实现近乎实时的决策制定,大量的数据和复杂的分析与计算可提供可靠信息。

PLINQ 通过在多核硬件上提供加速以带来业务优势的途径还有很多,这些仅仅是一小部分例子而已。其他领域也提供类似的机遇,例如医疗保健、经济、地质建模、科学计算、交通控制和模拟、游戏、人工智能、机器学习、语言分析等等。



虽然上述关于 PLINQ 并行化处理过程是完全透明的说法大部分属实,但是在少数情形中,并行性的使用可能会偏离上文中介绍的简单抽象。这些就是上文中略为提及的一些限制。庆幸的是,大多数限制都很细微,但是不管怎样,您必须了解它们。

任何引发异常的 lambda 或查询运算符都会立即停止顺序 LINQ 查询的执行。那是因为,在仅使用一个处理器来运行查询时,会按顺序逐个处理元素:如果某个运算符在其中一个元素上失败,则会立即引发异常,后续元素甚至不会纳入考虑。同样的情况对 PLINQ 并非如此。

为了说明这个问题,请看下面这个(精心设计的)查询:


object[] data = new object[] { "foo", null, null, null };
var q = data.Select(x => x.ToString());
foreach (var e in q) Console.WriteLine(e);


每次用 LINQ 运行此查询时,它对第一个数组元素运行 ToString 将成功,然后在对第二个元素尝试调用 ToString 时失败,引发 NullReferenceException。第三或第四个元素永远不会得到机会。然而,当使用多处理器时,象使用 PLINQ 那样,存在并行产生多个异常的可能性。根据 PLINQ 决定细分该问题的方法,您可能会看到第 1、第 2 和第 3 个元素同时失败,或者这三个元素的任何组合,可能是第 3 个元素失败,而非第 1 或第 2 个元素。

为了解决这个问题,PLINQ 采用了与 LINQ 稍有不同的异常模型来通报失败。当某个 PLINQ 线程上产生异常时,系统首先会尝试尽快停止所有其他线程的运行。此处理过程是完全透明的。但是此过程是否能及时完成以防止其他异常的并发产生是不可知的,事实上,这些异常在 PLINQ 开始介入之时就可能已经存在。所有线程关闭后,产生的整个异常集都将被聚合到新的 System.Concurrency.MultipleFailuresException 对象中,而且将重新引发这个新的聚合异常对象。产生的每个异常以后都可通过类型 Exception[] 的 InnerExceptions 属性进行访问,包括不受干扰的堆栈跟踪。

实际上,当某个未处理的异常终止了某个查询的执行时,PLINQ 始终会引发一个 MultipleFailuresException,即使实际上只引发一个异常。在上述例子中,这意味着 PLINQ 始终会将 NullReferenceExceptions 封装到 MultipleFailuresException。如果不是的话,您必须编写多个捕获子句才能捕获某个特定类型的异常。显而易见,您通常不会捕获某些类型的异常,但是如果有这种想法,那么就必须编写下列代码并复制大量逻辑:


try
{
// query...
}
catch (NullReferenceException)
{ ... }
catch (MultipleFailuresException)
{ ... }


这不仅是个笨办法,而且开发人员容易丢三落四,导致仅在某些情况和配置下才会产生的 bug。

很遗憾,这会增加调试难度。如果某个异常得不到处理而您又开始使用调试程序,那么您一开始将中断并进入对 GetEnumerator 的调用(如果对查询结果调用 foreach),而不是最初引发异常的地方。这类似于目前 .NET Framewor 中的异步编程模型 (BeginXX/EndXX) 中发生的情况。令人欣慰的是,PLINQ 保留了原始堆栈跟踪,因此当展开 MultipleFailuresException 对象并查看其 InnerExceptions 属性时,会找到整个异常集,并有完整的原始堆栈跟踪可供使用。


输出结果排序

假设您用 LINQ 编写了下列代码:


int[] data = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data select x * 2).ToArray();


您能预测 data2 的内容吗?这个问题看起来如此简单,甚至根本都不需要考虑。每个人都会回答:{ 0, 2, 4, 6 }。但是,如果您按如下所示对代码稍作改动,则 data2 的内容可能会大有不同:


int[] data = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data.AsParallel() select x * 2).ToArray();


在此例中,{ 0, 2, 4, 6 } 是非常可能的,但是 { 6, 0, 2, 4 } 也有可能,或者可能是这四个数字的任何其他排列组合。

之所以如此,是因为 PLINQ 并行运行查询,结果在产生之后即变为可用,不管是使用 foreach 遍历查询还是使用 ToArray 将结果封送到某个数组。LINQ 排序只是其实现按顺序处理输入这一事实的附带结果。与之相反,PLINQ 排序是由并行工作单元的不确定性计划确定的,这种工作单元在两次执行之间有可能发生极大的变化。

这是由 PLINQ 团队制定的一个明确的设计决策。以前,查询并不保证任何与排序相关的问题。举例来说,对于 SQL Server™,除非您在查询文本中用子句指定了一个顺序,否则排序将取决于许多因素:查询中是否使用了索引、磁盘上的记录布局等等。事实上,它也可能是不确定性的,因为 SQL Server 也可以在查询评估中使用并行性!

由于用户经常需要保留排序顺序,并且需要为尝试从 LINQ 迁移的一些用户解决一些小困难,因此,PLINQ 提供了一种明确加入以保留顺序的方法。顺序保留只是确保在没有其他排序操作介入的情况下,输出元素之间的相对顺序与输入元素之间的相对顺序紧密相关。如果要确保上述查询的输出始终为 {0, 2, 4, 6 },那么可以使用下列查询来代替:


int[] data = new int[] { 0, 1, 2, 3 };
int[] data2 = (from x in data.AsParallel(QueryOptions.PreserveOrdering)
select x * 2).ToArray();


顺序保留并不是没有代价的。事实上,它可能在很大程度上影响查询的性能和扩展能力。这是因为 PLINQ 将从逻辑上在末尾插入一个排序操作,而排序是一个无法随处理器数量的增加而实现完美扩展的运算符。要了解这其中的含义,上述查询在逻辑上等同于下列查询:


int[] data = new int[] { 0, 1, 2, 3 };
int[] data2 = data.AsParallel().
Select((x, i) => new { x, i }). // remember indices
Select((x) => new { x * 2, i }). // (in original)
OrderBy((x) => x.i). // ensure order is preserved
Select((x) => x.x). // get rid of indices from output
ToArray(); // (in original)


PLINQ 为支持顺序保留所引入的步骤是红色的。正如您所看到的,这是一项巨大的工作! 很显然,与编写此版本的查询相比,PLINQ 实现此功能的方法要高效得多,但是步骤在逻辑上是等效的。


副作用
PLINQ 资源
Joe Duffy 有关 PLINQ 的博客文章
Channel 9 与 Anders Hejlsberg 关于 LINQ 的视频访谈
.NET Framework 开发人员中心的 LINQ 项目
LINQ 的演变及其对 C# 设计的影响,作者:Anson Horton(《MSDN 杂志》,2007 年 6 月)
MSDN 论坛上对的 LINQ 讨论

567

社区成员

发帖
与我相关
我的任务
社区描述
英特尔® 边缘计算,聚焦于边缘计算、AI、IoT等领域,为开发者提供丰富的开发资源、创新技术、解决方案与行业活动。
社区管理员
  • 英特尔技术社区
  • shere_lin
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧