65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 PLINQ 加速阻塞函数

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011年2月4日

CPOL

5分钟阅读

viewsIcon

9759

如何使用 PLINQ 加速阻塞函数

PLINQ DOP Speedup Comparison

引言

我一直在研究 .NET 4 中的新 PLINQ 和并行任务库,寻找各种在 .NET 2.0 中无法实现的方法。PLINQ 非常强大,并且可以使用 .NET 4 进行多线程编程的许多新方法。在本文中,我想讨论多年来我多次遇到的一个特定问题。如何加速受阻塞函数或长时间运行的 I/O 操作限制的多线程应用程序?

我开始研究这种方法来加速 VistaDB 引擎深处一些长时间运行的文件 I/O 例程。大多数时候,在我们继续工作之前,我们会被从磁盘的读取操作阻塞,但通常我们拥有所需块的部分内容。因此我们可以开始工作,然后在其余块加载后继续。使用传统的线程代码添加该逻辑既复杂又容易出错。幸运的是,PLINQ 提供了一种使某些此类操作非常简单的方法。

读取多个网站

在此示例中,我将读取 8 个网站的首页,然后对这些信息进行操作。这是一种非常简单的并行操作,可以很好地进行拆分。但是这些类型的长时间运行读取与许多应用程序中发生的情况非常相似。

关于 C# 4.0 in a Nutshell 书籍的题外话

我实际上是从 Joseph Albahari 的 C# 4.0 in a Nutshell 书中采用的这个例子(他也是优秀工具 LinqPAD 的作者)。虽然 1000 页的内容算不上是“nutshell”(简要概述),但对于已经了解 C# 并且只想浏览 C# 和 CLR 4 的开发人员来说,这是一本很棒的书。本书中的概念也涵盖了旧版本的 .NET,但对我来说,最有价值的部分是所有新的变化。

LINQ 表达式

好的,这个表达式将访问此列表中的 8 个网站,并获取每个网站的首页。页面的内容长度和内容类型随后存储在一个变量中,以便稍后在并行计算之外使用。

static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();

    var results = from site in new[]
    {
        "http://infinitecodex.com",
        "http://www.vistadb.net",
        "http://stackoverflow.com",
        "http://cornerstonedb.com",
        "http://www.bing.com/",
        "http://www.linqpad.net",
        "http://www.cnn.com",
        "http://www.microsoft.com"
     }
     let p = WebRequest.Create( new Uri(site)).GetResponse()
         select new
         {
             site,
             Length = p.ContentLength,
             ContentType = p.ContentType
         };

     foreach (var result in results)
     {
         Console.WriteLine("{0}:{1}:{2}", 
             result.site, result.Length, result.ContentType);
     }

     sw.Stop();

     Console.WriteLine("Total Time: {0}ms", sw.ElapsedMilliseconds);          
}

最初的运行没有使用任何并行扩展。只需遍历每个站点并获取首页,将 ContentLengthContentType 存储在临时变量 p 中。之后,我 foreach 遍历结果,将其输出到命令行。如果你删除此步骤,由于 LINQ 中的延迟执行,实际上什么也不会发生(你必须对集合执行某些操作才能真正运行它)。我将所有这些都包装在 Stopwatch 中,以便知道花费了多长时间。本文顶部的图表是我在每次方法运行 10 次后获得的三次最快时间。

正常执行的三次最快时间 (ms):1916、2103、1992。

添加并行 (PLINQ)

现在,让我们使用 PLINQ 来做这件事,看看它是否运行得更快。

我们唯一需要做的更改是在 let 语句上方添加一行代码,像这样

}
.AsParallel()
let p = WebRequest.Create( new Uri(site)).GetResponse()

就是这样,整个 LINQ 查询现在将并行运行。它更快了,但没有我们能达到的速度那么快。

使用 AsParallel() 的三次最快时间 (ms):745、790、814。

PLINQ 在底层所做的是创建一个线程池,并在我的 4 核机器上启动 4 个线程。但它不知道的是,这些操作中的每一个都是阻塞的,都在等待来自网站的 I/O。PLINQ 假设每个线程都有适量的 CPU 工作要做,因此它会阻止启动大量只会淹没 CPU 的线程。

我们如何告诉 .NET Framework 这些并行操作都不是 CPU 密集型的?

WithDegreeOfParallelism

来自 MSDN 帮助:WithDegreeOfParallelism<TSource> - 并行度是用于处理查询的并发执行任务的最大数量。

现在,这并没有用简单的英语解释你可以使用它来告诉框架该任务不是 CPU 密集型的。从技术上讲,你正在覆盖 PLINQ 的默认行为,并告诉它你知道应该允许多少个并行运行。

在这种情况下,我将设置为 8,因为我知道每个 CPU 核心的两个对象都不会占用我的系统。你可以设置的最大值为 64。现在,每个线程池都将尝试一次运行多个线程。我们如何在不产生大量任务切换开销的情况下做到这一点?因为对象都阻塞在 I/O 中。操作系统会将它们置于休眠状态并释放 CPU 以运行其他任务;我们只是要给每个任务更多的工作来让他们更忙碌。

同样,只需对第一个查询进行一行更改即可

}
.AsParallel().WithDegreeOfParallelism(8) // HERE
let p = WebRequest.Create( new Uri(site)).GetResponse()

设置 8 并行度的三次最快时间 (ms):543、578、589。

这比原始查询快了 3.5 倍,而且只更改了一行代码!

摘要

PLINQ 和 .NET Framework 4 为你提供了强大的功能,可以非常轻松地加速并行操作。在我的页面管理器应用程序中,通过本文中列出的技术,我能够将页面缓存管理器的性能提高 4.5 倍。通过将我的队列机制更改为新的并发类,我能够消除大量浪费在锁定上的空闲时间,并获得更高的性能,但这将在未来的某个时候发表另一篇博客文章。

© . All rights reserved.