C#5 多线程秘籍(三)
原文:
zh.annas-archive.org/md5/B7D7E52064DCCDC9755A7421EE8385A4译者:飞龙
第七章:使用 PLINQ
在本章中,我们将回顾不同的并行编程范式,如任务和数据并行性,并介绍数据并行性和并行 LINQ 查询的基础知识。您将学习:
-
使用 Parallel 类
-
并行化 LINQ 查询
-
调整 PLINQ 查询的参数
-
在 PLINQ 查询中处理异常
-
在 PLINQ 查询中管理数据分区
-
为 PLINQ 查询创建自定义聚合器
介绍
在.NET Framework 中,有一个称为并行框架的库子集,通常称为并行框架扩展(PFX),这是这些库的第一个版本的名称。并行框架是随.NET Framework 4.0 发布的,由三个主要部分组成:
-
任务并行库(TPL)
-
并发集合
-
并行 LINQ 或 PLINQ
通过本书,我们学习了如何并行运行多个任务并使它们相互同步。实际上,我们将程序分成一组任务,并且有不同的线程运行不同的任务。这种方法被称为任务并行性,到目前为止我们只学习了任务并行性。
想象一下,我们有一个程序,对一大批数据进行一些繁重的计算。并行化这个程序最简单的方法是将这批数据分成较小的块,对这些数据块进行并行计算,然后聚合这些计算的结果。这种编程模型称为数据并行性。
任务并行性具有最低的抽象级别。我们将程序定义为一组任务的组合,明确定义它们如何组合。以这种方式组成的程序可能非常复杂和详细。并行操作在程序的不同位置定义,随着程序的增长,程序变得更难理解和维护。这种使程序并行的方式被称为非结构化并行性。如果我们有复杂的并行逻辑,这就是要付出的代价。
然而,当我们有更简单的程序逻辑时,我们可以尝试将更多的并行化细节交给 PFX 库和 C#编译器。例如,我们可以说,“我想并行运行这三种方法,我不在乎这种并行化的具体细节; 让.NET 基础设施决定细节”。这提高了抽象级别,因为我们不必提供关于我们如何并行化的详细描述。这种方法被称为结构化并行性,因为并行化通常是一种声明,并且每种并行化情况在程序中的一个地方被定义。
注意
可能会有一种印象,即非结构化并行性是一种不好的实践,而应该始终使用结构化并行性。我想强调这是不正确的。结构化并行性确实更易于维护,并且在可能的情况下更受青睐,但它是一种不太通用的方法。一般来说,有许多情况下我们根本无法使用它,使用 TPL 任务并行性以非结构化方式是完全可以的。
任务并行库有一个Parallel类,提供了结构化并行性的 API。这仍然是 TPL 的一部分,但我们将在本章中进行审查,因为它是从较低抽象级别向较高抽象级别过渡的一个完美例子。当我们使用Parallel类的 API 时,我们不需要提供如何分区我们的工作的细节。但是,我们仍然需要明确定义如何从分区结果中得出一个单一的结果。
PLINQ 具有最高的抽象级别。它会自动将数据分成块,并决定我们是否真的需要并行化查询,或者使用通常的顺序查询处理效果更好。然后,PLINQ 基础设施会负责将分区结果合并在一起。程序员可以调整许多选项来优化查询,实现最佳性能和结果。
在本章中,我们将介绍Parallel类的 API 用法和许多不同的 PLINQ 选项,比如使 LINQ 查询并行化,设置执行模式和调整 PLINQ 查询的并行度,处理查询项顺序以及处理 PLINQ 异常。我们还将学习如何管理 PLINQ 查询的数据分区。
使用 Parallel 类
本示例展示了如何使用Parallel类的 API。我们将学习如何并行调用方法,如何执行并行循环,并调整并行化机制。
准备工作
要按照这个步骤,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter7\Recipe1中找到。
如何做...
要并行调用方法,执行并行循环,并使用Parallel类调整并行化机制,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs文件中添加以下using指令:
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
- 在
Main方法下方添加以下代码片段:
static string EmulateProcessing(string taskName)
{
Thread.Sleep(TimeSpan.FromMilliseconds(new Random(DateTime.Now.Millisecond).Next(250, 350)));
Console.WriteLine("{0} task was processed on a thread id {1}",taskName, Thread.CurrentThread.ManagedThreadId);
return taskName;
}
- 在
Main方法中添加以下代码片段:
Parallel.Invoke(() => EmulateProcessing("Task1"),() => EmulateProcessing("Task2"),() => EmulateProcessing("Task3")
);
var cts = new CancellationTokenSource();
var result = Parallel.ForEach(
Enumerable.Range(1, 30),
new ParallelOptions
{
CancellationToken = cts.Token,
MaxDegreeOfParallelism = Environment.ProcessorCount,
TaskScheduler = TaskScheduler.Default
},
(i, state) =>
{
Console.WriteLine(i);
if (i == 20)
{
state.Break();
Console.WriteLine("Loop is stopped: {0}", state.IsStopped);
}
});
Console.WriteLine("---");
Console.WriteLine("IsCompleted: {0}", result.IsCompleted);
Console.WriteLine("Lowest break iteration: {0}", result.LowestBreakIteration);
- 运行程序。
它是如何工作的...
该程序演示了Parallel类的不同特性。Invoke方法允许我们在并行运行多个操作,而不像在任务并行库中定义任务那样麻烦。Invoke方法会阻塞其他线程,直到所有操作完成,这是一个常见且方便的场景。
下一个功能是并行循环,使用For和ForEach方法定义。我们将仔细研究ForEach,因为它与For非常相似。关于并行ForEach循环,您可以处理任何IEnumerable集合,通过将动作委托应用于每个集合项来并行处理。我们能够提供几个选项,自定义并行化行为,并获得一个显示循环是否成功完成的结果。
为了调整我们的并行循环,我们向ForEach方法提供了ParallelOptions类的实例。这允许我们使用CancellationToken取消循环,限制最大并行度(可以并行运行的最大操作数),并提供自定义的TaskScheduler类来调度动作任务。动作可以接受额外的ParallelLoopState参数,这对于中断循环或检查循环当前发生了什么非常有用。
有两种方法可以使用此状态停止并行循环。我们可以使用Break或Stop方法。Stop方法告诉循环停止处理更多的工作,并将并行循环状态的IsStopped属性设置为true。Break方法在此后停止迭代,但最初的迭代将继续工作。在这种情况下,循环结果的LowestBreakIteration属性将包含调用Break方法的最低循环迭代的编号。
使 LINQ 查询并行化
本示例将描述如何使用 PLINQ 使查询并行化,以及如何从并行查询返回到顺序处理。
准备工作
要按照这个步骤,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter7\Recipe2中找到。
如何做...
要使用 PLINQ 使查询并行化,并从并行查询返回到顺序处理,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#
控制台应用程序项目。 -
在
Program.cs文件中添加以下using指令:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
- 在
Main方法下面添加以下代码片段:
static void PrintInfo(string typeName)
{
Thread.Sleep(TimeSpan.FromMilliseconds(150));
Console.WriteLine("{0} type was printed on a thread id {1}", typeName, Thread.CurrentThread.ManagedThreadId);
}
static string EmulateProcessing(string typeName)
{
Thread.Sleep(TimeSpan.FromMilliseconds(150));
Console.WriteLine("{0} type was processed on a thread id {1}",typeName, Thread.CurrentThread.ManagedThreadId);
return typeName;
}
static IEnumerable<string> GetTypes()
{
return from assembly in AppDomain.CurrentDomain.GetAssemblies()from type in assembly.GetExportedTypes()where type.Name.StartsWith("Web")select type.Name;
}
- 在
Main方法内添加以下代码片段:
var sw = new Stopwatch();
sw.Start();
var query = from t in GetTypes()select EmulateProcessing(t);
foreach (string typeName in query)
{
PrintInfo(typeName);
}
sw.Stop();
Console.WriteLine("---");
Console.WriteLine("Sequential LINQ query.");
Console.WriteLine("Time elapsed: {0}", sw.Elapsed);
Console.WriteLine("Press ENTER to continue....");
Console.ReadLine();
Console.Clear();
sw.Reset();
sw.Start();
var parallelQuery = from t in ParallelEnumerable.AsParallel(GetTypes())select EmulateProcessing(t);
foreach (string typeName in parallelQuery)
{
PrintInfo(typeName);
}
sw.Stop();
Console.WriteLine("---");
Console.WriteLine("Parallel LINQ query. The results are being merged on a single thread");
Console.WriteLine("Time elapsed: {0}", sw.Elapsed);
Console.WriteLine("Press ENTER to continue....");
Console.ReadLine();
Console.Clear();
sw.Reset();
sw.Start();
parallelQuery = from t in GetTypes().AsParallel()select EmulateProcessing(t);
parallelQuery.ForAll(PrintInfo);
sw.Stop();
Console.WriteLine("---");
Console.WriteLine("Parallel LINQ query. The results are being processed in parallel");
Console.WriteLine("Time elapsed: {0}", sw.Elapsed);
Console.WriteLine("Press ENTER to continue....");
Console.ReadLine();
Console.Clear();
sw.Reset();
sw.Start();
query = from t in GetTypes().AsParallel().AsSequential()select EmulateProcessing(t);
foreach (var typeName in query)
{
PrintInfo(typeName);
}
sw.Stop();
Console.WriteLine("---");
Console.WriteLine("Parallel LINQ query, transformed into sequential.");
Console.WriteLine("Time elapsed: {0}", sw.Elapsed);
Console.WriteLine("Press ENTER to continue....");
Console.ReadLine();
Console.Clear();
- 运行程序。
它是如何工作的...
当程序运行时,我们创建一个 LINQ 查询,该查询使用反射 API 从当前应用程序域中加载的程序集中获取所有以“Web”开头的类型。我们使用EmulateProcessing和PrintInfo方法模拟处理每个项目和打印项目的延迟。我们还使用Stopwatch类来测量每个查询的执行时间。
首先运行一个通常的顺序 LINQ 查询。这里没有并行化,所以一切都在当前线程上运行。查询的第二个版本明确使用了ParallelEnumerable类。ParallelEnumerable包含了 PLINQ 逻辑实现,并且组织为IEnumerable集合功能的一些扩展方法。通常我们不会显式使用这个类;这里是为了说明 PLINQ 实际上是如何工作的。第二个版本并行运行EmulateProcessing;然而,默认情况下结果会在单个线程上合并,因此查询执行时间应该比第一个版本少几秒。
第三个版本展示了如何使用AsParallel方法以声明方式并行运行 LINQ 查询。我们不关心这里的实现细节,只是说明我们想要并行运行。然而,这个版本的关键区别在于我们使用ForAll方法来打印查询结果。它在相同的线程上运行操作以处理查询中的所有项目,跳过结果合并步骤。这使我们也可以并行运行PrintInfo,这个版本甚至比上一个版本运行得更快。
最后一个示例展示了如何使用AsSequential方法将 PLINQ 查询转换回顺序。我们可以看到这个查询的运行方式与第一个查询完全相同。
调整 PLINQ 查询的参数
该示例展示了如何使用 PLINQ 查询来管理并行处理选项,以及这些选项在查询执行期间可能会产生的影响。
准备工作
要执行此示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter7\Recipe3中找到。
如何做...
要了解如何使用 PLINQ 查询来管理并行处理选项及其影响,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs文件中添加以下using指令:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
- 在
Main方法下面添加以下代码片段:
static string EmulateProcessing(string typeName)
{
Thread.Sleep(TimeSpan.FromMilliseconds(new Random(DateTime.Now.Millisecond).Next(250,350)));
Console.WriteLine("{0} type was processed on a thread id {1}",typeName, Thread.CurrentThread.ManagedThreadId);
return typeName;
}
static IEnumerable<string> GetTypes()
{
return from assembly in AppDomain.CurrentDomain.GetAssemblies()from type in assembly.GetExportedTypes()where type.Name.StartsWith("Web")orderby type.Name.Lengthselect type.Name;
}
- 在
Main方法内添加以下代码片段:
var parallelQuery = from t in GetTypes().AsParallel()select EmulateProcessing(t);
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
try
{
parallelQuery.WithDegreeOfParallelism(Environment.ProcessorCount).WithExecutionMode(ParallelExecutionMode.ForceParallelism).WithMergeOptions (ParallelMergeOptions.Default).WithCancellation(cts.Token).ForAll(Console.WriteLine);
}
catch (OperationCanceledException)
{
Console.WriteLine("---");
Console.WriteLine("Operation has been canceled!");
}
Console.WriteLine("---");
Console.WriteLine("Unordered PLINQ query execution");
var unorderedQuery = from i in ParallelEnumerable.Range(1, 30) select i;
foreach (var i in unorderedQuery)
{
Console.WriteLine(i);
}
Console.WriteLine("---");
Console.WriteLine("Ordered PLINQ query execution");
var orderedQuery = from i in ParallelEnumerable.Range(1, 30).AsOrdered() select i;
foreach (var i in orderedQuery)
{
Console.WriteLine(i);
}
- 运行程序。
它是如何工作的...
该程序演示了程序员可以使用的不同有用的 PLINQ 选项。我们首先创建一个 PLINQ 查询,然后创建另一个提供 PLINQ 调整的查询。
让我们先从取消开始。为了能够取消 PLINQ 查询,有一个接受取消令牌对象的WithCancellation方法。在这里,我们在三秒后发出取消令牌信号,这导致查询中的OperationCanceledException和其余工作的取消。
然后,我们可以为查询指定并行度。这是执行查询时将使用的精确并行分区的数量。在第一个示例中,我们使用了Parallel.ForEach循环,它具有最大并行度选项。这是不同的,因为它指定了最大分区值,但如果基础设施决定最好使用较少的并行性来节省资源并实现最佳性能,可能会有更少的分区。
另一个有趣的选项是使用WithExecutionMode方法覆盖查询执行模式。如果 PLINQ 基础设施决定并行化查询只会增加更多的开销,并且实际上运行得更慢,它可以以顺序模式处理一些查询。我们可以强制查询并行运行。
为了调整查询结果处理,我们有WithMergeOptions方法。默认模式是在从查询中返回结果之前,由 PLINQ 基础设施选择的一定数量的结果进行缓冲。如果查询需要大量时间,关闭结果缓冲以尽快获得结果更为合理。
最后一个选项是AsOrdered方法。当我们使用并行执行时,集合中的项目顺序可能不会被保留。在处理更早的项目之前,集合中的后续项目可能会被处理。为了防止这种情况,我们需要在并行查询上调用AsOrdered,明确告诉 PLINQ 基础设施我们打算保留项目顺序进行处理。
在 PLINQ 查询中处理异常
这个教程将描述如何处理 PLINQ 查询中的异常。
准备工作
要按照这个教程进行,你需要 Visual Studio 2012。没有其他先决条件。这个教程的源代码可以在BookSamples\Chapter7\Recipe4中找到。
如何做...
要了解如何处理 PLINQ 查询中的异常,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs文件中添加以下using指令:
using System;
using System.Collections.Generic;
using System.Linq;
- 在
Main方法中添加以下代码片段:
IEnumerable<int> numbers = Enumerable.Range(-5, 10);
var query = from number in numbersselect 100 / number;
try
{
foreach(var n in query)
Console.WriteLine(n);
}
catch (DivideByZeroException)
{
Console.WriteLine("Divided by zero!");
}
Console.WriteLine("---");
Console.WriteLine("Sequential LINQ query processing");
Console.WriteLine();
var parallelQuery = from number in numbers.AsParallel()select 100 / number;
try
{
parallelQuery.ForAll(Console.WriteLine);
}
catch (DivideByZeroException)
{
Console.WriteLine("Divided by zero - usual exception handler!");
}
catch (AggregateException e)
{
e.Flatten().Handle(ex =>
{
if (ex is DivideByZeroException)
{
Console.WriteLine("Divided by zero - aggregate exception handler!");
return true;
}
return false;
});
}
Console.WriteLine("---");
Console.WriteLine("Parallel LINQ query processing and results merging");
- 运行程序。
它是如何工作的...
首先,我们对从-5 到 4 的数字范围进行了一个通常的 LINQ 查询。当我们除以零时,我们得到DivideByZeroException,并像通常一样在 try/catch 块中处理它。
然而,当我们使用AsParallel时,我们将得到AggregateException,因为现在我们是在并行运行,利用了后台的任务基础设施。AggregateException将包含在运行 PLINQ 查询时发生的所有异常。为了处理内部的DivideByZeroException类,我们使用了在第五章的处理异步操作中的异常教程中解释过的Flatten和Handle方法,使用 C# 5.0。
注意
很容易忘记当我们处理聚合异常时,内部有多个异常是非常常见的情况。如果你忘记处理所有这些异常,异常将冒泡并且应用程序将停止工作。
在 PLINQ 查询中管理数据分区
这个教程展示了如何创建一个非常基本的自定义分区策略,以特定方式并行化 LINQ 查询。
准备工作
要按照这个教程进行,你需要 Visual Studio 2012。没有其他先决条件。这个教程的源代码可以在BookSamples\Chapter7\Recipe5中找到。
如何做...
要了解如何创建一个非常基本的自定义分区策略来并行化 LINQ 查询,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs文件中添加以下using指令:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
- 在
Main方法下面添加以下代码片段:
static void PrintInfo(string typeName)
{
Thread.Sleep(TimeSpan.FromMilliseconds(150));
Console.WriteLine("{0} type was printed on a thread id {1}",typeName, Thread.CurrentThread.ManagedThreadId);
}
static string EmulateProcessing(string typeName)
{
Thread.Sleep(TimeSpan.FromMilliseconds(150));
Console.WriteLine("{0} type was processed on a thread id {1}. Has {2} length.",typeName, Thread.CurrentThread.ManagedThreadId, typeName.Length % 2 == 0 ? "even" : "odd");
return typeName;
}
static IEnumerable<string> GetTypes()
{
var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetExportedTypes());
return from type in types where type.Name.StartsWith("Web")select type.Name;
}
public class StringPartitioner : Partitioner<string>
{
private readonly IEnumerable<string> _data;
public StringPartitioner(IEnumerable<string> data)
{
_data = data;
}
public override bool SupportsDynamicPartitions
{
get
{
return false;
}
}
public override IList<IEnumerator<string>> GetPartitions(int partitionCount)
{
var result = new List<IEnumerator<string>>(2);
result.Add(CreateEnumerator(true));
result.Add(CreateEnumerator(false));
return result;
}
IEnumerator<string> CreateEnumerator(bool isEven)
{
foreach (var d in _data)
{
if (!(d.Length % 2 == 0 ^ isEven))
yield return d;
}
}
}
- 在
Main方法中添加以下代码片段:
var partitioner = new StringPartitioner(GetTypes());
var parallelQuery = from t in partitioner.AsParallel()select EmulateProcessing(t);
parallelQuery.ForAll(PrintInfo);
- 运行程序。
它是如何工作的...
为了说明我们能够为 PLINQ 查询选择自定义分区策略,我们创建了一个非常简单的分区器,以并行方式处理奇数和偶数长度的字符串。为了实现这一点,我们从标准基类Partitioner<T>中使用string作为类型参数派生出我们自定义的StringPartitioner类。
我们声明只支持静态分区,通过覆盖SupportsDynamicPartitions属性并将其设置为false。这意味着我们预定义了我们的分区策略。这是对初始集合进行分区的一种简单方法,但根据集合中的数据内容可能效率低下。例如,在我们的情况下,如果我们有许多奇数长度的字符串和只有一个偶数长度的字符串,其中一个线程将提前完成并且不会帮助处理奇数长度的字符串。另一方面,动态分区意味着我们在飞行中对初始集合进行分区,平衡工作负载在工作线程之间。
然后我们实现了GetPartitions方法,在其中定义了两个迭代器。第一个从源集合返回奇数长度的字符串,第二个返回偶数长度的字符串。最后,我们创建了我们的分区器的实例,并对其执行了 PLINQ 查询。我们可以看到不同的线程处理奇数长度和偶数长度的字符串。
为 PLINQ 查询创建自定义聚合器
本示例演示了如何为 PLINQ 查询创建自定义聚合函数。
准备就绪
要按照本示例进行操作,您需要 Visual Studio 2012。没有其他先决条件。本示例的源代码可以在BookSamples\Chapter7\Recipe6中找到。
如何做...
了解 PLINQ 查询的自定义聚合函数的工作原理,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs文件中添加以下using指令:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
- 在
Main方法下面添加以下代码片段:
static ConcurrentDictionary<char, int> AccumulateLettersInformation(ConcurrentDictionary<char, int> taskTotal , string item)
{
foreach (var c in item)
{
if (taskTotal.ContainsKey(c))
{
taskTotal[c] = taskTotal[c] + 1;
}
else
{
taskTotal[c] = 1;
}
}
Console.WriteLine("{0} type was aggregated on a thread id {1}",item, Thread.CurrentThread.ManagedThreadId);
return taskTotal;
}
static ConcurrentDictionary<char, int> MergeAccumulators(ConcurrentDictionary<char, int> total, ConcurrentDictionary<char, int> taskTotal)
{
foreach (var key in taskTotal.Keys)
{
if (total.ContainsKey(key))
{
total[key] = total[key] + taskTotal[key];
}
else
{
total[key] = taskTotal[key];
}
}
Console.WriteLine("---");
Console.WriteLine("Total aggregate value was calculated on a thread id {0}",Thread.CurrentThread.ManagedThreadId);
return total;
}
static IEnumerable<string> GetTypes()
{
var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetExportedTypes());
return from type in typeswhere type.Name.StartsWith("Web")select type.Name;
}
- 在
Main方法中添加以下代码片段:
var parallelQuery = from t in GetTypes().AsParallel() select t;
var parallelAggregator = parallelQuery.Aggregate(() => new ConcurrentDictionary<char, int>(),(taskTotal, item) => AccumulateLettersInformation(taskTotal, item), (total, taskTotal) => MergeAccumulators(total, taskTotal), total => total);
Console.WriteLine();
Console.WriteLine("There were the following letters in type names:");
var orderedKeys = from k in parallelAggregator.Keysorderby parallelAggregator[k] descending select k;
foreach (var c in orderedKeys)
{
Console.WriteLine("Letter '{0}' ---- {1} times", c, parallelAggregator[c]);
}
- 运行程序。
它是如何工作的...
在这里,我们实现了能够处理 PLINQ 查询的自定义聚合机制。为了实现这一点,我们必须了解,由于查询正在由多个任务同时并行处理,我们需要提供机制来并行聚合每个任务的结果,然后将这些聚合值合并为一个单一的结果值。
在本示例中,我们编写了一个聚合函数,用于计算 PLINQ 查询中字母的数量,该查询返回IEnumerable<string>集合。它计算每个集合项中的所有字母。为了说明并行聚合过程,我们打印出了关于哪个线程处理聚合的每个部分的信息。
我们使用ParallelEnumerable类中定义的Aggregate扩展方法对 PLINQ 查询结果进行聚合。它接受四个参数,每个参数都是执行聚合过程不同部分的函数。第一个是构造聚合器的空初始值的工厂。它也被称为种子值。
注意
请注意,提供给Aggregate方法的第一个值实际上不是聚合器函数的初始种子值,而是一个构造此初始种子值的工厂方法。如果您只提供一个实例,它将在所有并行运行的分区中使用,这将导致不正确的结果。
第二个函数将每个集合项聚合到分区聚合对象中。我们使用AccumulateLettersInformation方法实现此函数。它遍历字符串并计算其中的字母。这里聚合对象对于并行运行的每个查询分区都是不同的,这就是为什么我们称它们为taskTotal。
第三个函数是一个高级别的聚合函数,它从分区中获取聚合器对象并将其合并到全局聚合器对象中。我们使用MergeAccumulators方法实现它。最后一个函数是一个选择器函数,指定我们需要从全局聚合器对象中获取的确切数据。
最后,我们打印出聚合结果,并按集合项中最常用的字母对其进行排序。
第八章:反应扩展
在本章中,我们将看看另一个有趣的.NET 库,它可以帮助我们创建异步程序,即反应扩展(或 Rx)。您将学习以下配方:
-
将集合转换为异步
Observable -
编写自定义
Observable -
使用
Subjects -
创建一个
Observables对象 -
使用 LINQ 查询对
Observable集合 -
使用 Rx 创建异步操作
介绍
正如我们已经了解的,有几种方法可以在.NET 和 C#中创建异步程序。其中之一是基于事件的异步模式,这在前几章中已经提到过。引入事件的最初目标是简化实现Observer设计模式。这种模式通常用于在对象之间实现通知。
当我们讨论任务并行库时,我们注意到事件的主要缺点是它们无法有效地相互组合。另一个缺点是基于事件的异步模式不应该用来处理通知的顺序。想象一下,我们有一个IEnumerable<string>给我们字符串值。然而,当我们遍历它时,我们不知道一个迭代会花费多少时间。它可能很慢,如果我们使用常规的foreach或其他同步迭代结构,我们将阻塞我们的线程,直到我们有下一个值。这种情况被称为拉取式方法,当我们作为客户端从生产者那里拉取值时。
另一种方法是推送式方法,当生产者通知客户端有新值时。这允许将工作卸载给生产者,而客户端在等待另一个值时可以自由做任何其他事情。因此,目标是获得类似于异步版本的IEnumerable,它产生一系列值,并在序列中的每个项目完成时通知消费者,或者在抛出异常时通知消费者。
.NET Framework 从 4.0 版本开始包含了接口IObservable<out T>和IObserver<in T>的定义,它们一起代表了异步推送式集合及其客户端。它们来自一个名为 Reactive Extensions(简称 Rx)的库,该库是在微软内部创建的,旨在帮助有效地组合事件序列以及实际上所有其他类型的使用可观察集合的异步程序。这些接口被包含在.NET Framework 中,但它们的实现和所有其他机制仍然分别在 Rx 库中分发。
注意
反应扩展首先是一个跨平台库。有.NET 3.5、Silverlight 和 Windows Phone 的库。它也可用于 JavaScript、Ruby 和 Python。它也是开源的;你可以在 CodePlex 网站上找到.NET 的反应扩展源代码,也可以在 GitHub 上找到其他实现。
最令人惊讶的是,可观察集合与 LINQ 兼容,因此我们能够使用声明性查询以异步方式转换和组合这些集合。这也使得可以使用扩展方法来为 Rx 程序添加功能,就像在通常的 LINQ 提供程序中使用的方式一样。反应扩展还支持从所有异步编程模式(包括异步编程模型、基于事件的异步模式和任务并行库)过渡到可观察集合,并支持其自己的运行异步操作的方式,这仍然与 TPL 非常相似。
Reactive Extensions 库是一个非常强大和复杂的工具,值得写一本单独的书。在本章中,我想回顾最有用的场景,即如何有效地处理异步事件序列。我们将观察 Reactive Extensions 框架的关键类型,学习如何创建序列并以不同的方式操纵它们,最后,检查我们如何使用 Reactive Extensions 来运行异步操作并管理它们的选项。
将集合转换为异步 Observable
本篇将介绍如何从Enumerable类创建一个可观察集合,并异步处理它。
准备工作
要完成本篇,您需要 Visual Studio 2012。不需要其他先决条件。本篇的源代码可以在BookSamples\Chapter8\Recipe1中找到。
如何做...
要理解如何从Enumerable类创建一个可观察集合并异步处理它,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
将对Reactive Extensions Main Library NuGet 包添加引用。
-
在项目中右键单击引用文件夹,然后选择**管理 NuGet 包...**菜单选项。
-
现在添加您首选的Reactive Extensions - Main Library NuGet 包引用。您可以在管理 NuGet 包对话框中使用搜索,如下面的屏幕截图所示:
- 在
Program.cs文件中,添加以下using指令:
using System;
using System.Collections.Generic;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Threading;
- 在
Main方法下面添加以下代码片段:
static IEnumerable<int> EnumerableEventSequence()
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(0.5));
yield return i;
}
}
- 在
Main方法中添加以下代码片段:
foreach (int i in EnumerableEventSequence())
{
Console.Write(i);
}
Console.WriteLine();
Console.WriteLine("IEnumerable");
IObservable<int> o = EnumerableEventSequence().ToObservable();
using (IDisposable subscription = o.Subscribe(Console.Write))
{
Console.WriteLine();
Console.WriteLine("IObservable");
}
o = EnumerableEventSequence().ToObservable().SubscribeOn(TaskPoolScheduler.Default);
using (IDisposable subscription = o.Subscribe(Console.Write))
{
Console.WriteLine();
Console.WriteLine("IObservable async");
Console.ReadLine();
}
- 运行程序。
它是如何工作的...
我们使用EnumerableEventSequence方法模拟一个慢的可枚举集合。然后我们在通常的foreach循环中对其进行迭代,我们可以看到它实际上是慢的;我们等待每次迭代完成。
然后,我们使用 Reactive Extensions 库中的ToObservable扩展方法将这个可枚举集合转换为Observable。接下来,我们订阅这个可观察集合的更新,提供Console.Write方法作为操作,这将在每次集合更新时执行。结果我们得到了与之前完全相同的行为;我们等待每次迭代完成,因为我们使用主线程订阅更新。
注意
我们将订阅对象包装到使用语句中。虽然这并不总是必要的,但处理订阅是一个良好的实践,可以避免生命周期相关的错误。
为了使程序异步,我们使用SubscribeOn方法,并提供 TPL 任务池调度程序。这个调度程序将订阅到 TPL 任务池,从主线程卸载工作。这使我们能够保持 UI 的响应性,并在集合更新时做其他事情。要检查这种行为,您可以从代码中删除最后一个Console.ReadLine调用。这样做会立即结束我们的主线程,这将迫使所有后台线程(包括 TPL 任务池工作线程)也结束,并且我们将得不到异步集合的输出。
如果我们使用任何 UI 框架,我们必须只在 UI 线程内与 UI 控件交互。为了实现这一点,我们应该使用相应的调度程序的ObserveOn方法。对于 Windows Presentation Foundation,我们有DispatcherScheduler类和在名为 Rx-XAML 的单独 NuGet 包中定义的ObserveOnDispatcher扩展方法,或者 Reactive Extensions XAML 支持库。对于其他平台,也有相应的单独 NuGet 包。
编写自定义 Observable
本篇将描述如何实现IObservable<in T>和IObserver<out T>接口以获取自定义的 Observable 序列并正确消耗它。
准备工作
要执行此配方,您需要 Visual Studio 2012。不需要其他先决条件。此配方的源代码可以在BookSamples\Chapter8\Recipe2中找到。
操作步骤...
要理解如何实现IObservable<in T>和IObserver<out T>接口以获取自定义的 Observable 序列并消费它,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
添加对Reactive Extensions Main Library NuGet 包的引用。有关如何执行此操作的详细信息,请参阅将集合转换为异步可观察对象配方。
-
在
Program.cs文件中,添加以下using指令:
using System;
using System.Collections.Generic;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
- 在
Main方法下方添加以下代码片段:
class CustomObserver : IObserver<int>
{
public void OnNext(int value)
{
Console.WriteLine("Next value: {0}; Thread Id: {1}", value, Thread.CurrentThread.ManagedThreadId);
}
public void OnError(Exception error)
{
Console.WriteLine("Error: {0}", error.Message);
}
public void OnCompleted()
{
Console.WriteLine("Completed");
}
}
class CustomSequence : IObservable<int>
{
private readonly IEnumerable<int> _numbers;
public CustomSequence(IEnumerable<int> numbers)
{
_numbers = numbers;
}
public IDisposable Subscribe(IObserver<int> observer)
{
foreach (var number in _numbers)
{
observer.OnNext(number);
}
observer.OnCompleted();
return Disposable.Empty;
}
}
- 在
Main方法内添加以下代码片段:
var observer = new CustomObserver();
var goodObservable = new CustomSequence(new[] {1, 2, 3, 4, 5});
var badObservable = new CustomSequence(null);
using (IDisposable subscription = goodObservable.Subscribe(observer))
{
}
using (IDisposable subscription = goodObservable.SubscribeOn(TaskPoolScheduler.Default).Subscribe(observer))
{
Thread.Sleep(100);
}
using (IDisposable subscription = badObservable.SubscribeOn(TaskPoolScheduler.Default).Subscribe(observer))
{
Console.ReadLine();
}
- 运行程序。
工作原理...
在这里,我们首先实现了我们的观察者,简单地将来自可观察集合的下一个项目的信息打印到控制台上,错误,或者序列完成。这是一个非常简单的消费者代码,没有什么特别之处。
有趣的部分是我们的可观察集合实现。我们在构造函数中接受一个数字的枚举,并且故意不检查它是否为空。当我们有一个订阅的观察者时,我们遍历这个集合,并通知观察者枚举中的每个项目。
然后我们演示了实际的订阅。正如我们所看到的,通过调用SubscribeOn方法实现了异步,这是一个扩展方法,包含了异步订阅逻辑。我们不关心可观察集合中的异步性;我们使用了 Reactive Extensions 库中的标准实现。
当我们订阅普通的可观察集合时,我们只会得到其中的所有项目。现在它是异步的,所以我们需要等待一段时间,等待异步操作完成,然后才打印消息并等待用户输入。
最后,我们尝试订阅下一个可观察集合,我们正在遍历一个空枚举,因此会得到一个空引用异常。我们看到异常已经被正确处理,并且执行了OnError方法来打印出错误详情。
使用 Subjects
这个配方展示了如何使用 Reactive Extensions 库中的 Subject 类型家族。
准备工作
要执行此配方,您需要 Visual Studio 2012。不需要其他先决条件。此配方的源代码可以在BookSamples\Chapter8\Recipe3中找到。
操作步骤...
要理解如何使用 Reactive Extensions 库中的 Subject 类型家族,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
添加对Reactive Extensions Main Library NuGet 包的引用。有关如何执行此操作的详细信息,请参阅将集合转换为异步可观察对象配方。
-
在
Program.cs文件中,添加以下using指令:
using System;
using System.Reactive.Subjects;
using System.Threading;
- 在
Main方法下方添加以下代码片段:
static IDisposable OutputToConsole<T>(IObservable<T> sequence)
{
return sequence.Subscribe(obj => Console.WriteLine("{0}", obj), ex => Console.WriteLine("Error: {0}", ex.Message), () => Console.WriteLine("Completed"));
}
- 在
Main方法内添加以下代码片段:
Console.WriteLine("Subject");
var subject = new Subject<string>();
subject.OnNext("A");
using (var subscription = OutputToConsole(subject))
{
subject.OnNext("B");
subject.OnNext("C");
subject.OnNext("D");
subject.OnCompleted();
subject.OnNext("Will not be printed out");
}
Console.WriteLine("ReplaySubject");
var replaySubject = new ReplaySubject<string>();
replaySubject.OnNext("A");
using (var subscription = OutputToConsole(replaySubject))
{
replaySubject.OnNext("B");
replaySubject.OnNext("C");
replaySubject.OnNext("D");
replaySubject.OnCompleted();
}
Console.WriteLine("Buffered ReplaySubject");
var bufferedSubject = new ReplaySubject<string>(2);
bufferedSubject.OnNext("A");
bufferedSubject.OnNext("B");
bufferedSubject.OnNext("C");
using (var subscription = OutputToConsole(bufferedSubject))
{
bufferedSubject.OnNext("D");
bufferedSubject.OnCompleted();
}
Console.WriteLine("Time window ReplaySubject");
var timeSubject = new ReplaySubject<string>(TimeSpan.FromMilliseconds(200));
timeSubject.OnNext("A");
Thread.Sleep(TimeSpan.FromMilliseconds(100));
timeSubject.OnNext("B");
Thread.Sleep(TimeSpan.FromMilliseconds(100));
timeSubject.OnNext("C");
Thread.Sleep(TimeSpan.FromMilliseconds(100));
using (var subscription = OutputToConsole(timeSubject))
{
Thread.Sleep(TimeSpan.FromMilliseconds(300));
timeSubject.OnNext("D");
timeSubject.OnCompleted();
}
Console.WriteLine("AsyncSubject");
var asyncSubject = new AsyncSubject<string>();
asyncSubject.OnNext("A");
using (var subscription = OutputToConsole(asyncSubject))
{
asyncSubject.OnNext("B");
asyncSubject.OnNext("C");
asyncSubject.OnNext("D");
asyncSubject.OnCompleted();
}
Console.WriteLine("BehaviorSubject");
var behaviorSubject = new BehaviorSubject<string>("Default");
using (var subscription = OutputToConsole(behaviorSubject))
{
behaviorSubject.OnNext("B");
behaviorSubject.OnNext("C");
behaviorSubject.OnNext("D");
behaviorSubject.OnCompleted();
}
- 运行程序。
工作原理...
在这个程序中,我们查看了 Subject 类型家族的不同变体。Subject 代表了IObservable和IObserver的实现。在不同的代理场景中,当我们想要将来自多个来源的事件转换为一个流,或者反之亦然,将事件序列广播给多个订阅者时,这是非常有用的。Subject 也非常方便用于对反应扩展进行实验。
让我们从基本的 Subject 类型开始。它会在订阅者订阅后立即将事件序列重新传递给订阅者。在我们的情况下,A字符串不会被打印出来,因为订阅发生在它被传输之后。此外,当我们在Observable上调用OnCompleted或OnError方法时,它会停止进一步传递事件序列,因此最后一个字符串也不会被打印出来。
下一个类型ReplaySubject非常灵活,允许我们实现三种额外的场景。首先,它可以缓存从它们广播开始的所有事件,如果我们稍后订阅,我们将首先得到所有先前的事件。这种行为在第二个例子中有所体现。在这里,我们将在控制台上看到所有四个字符串,因为第一个事件将被缓存并转换给后来的订阅者。
然后我们可以为ReplaySubject指定缓冲区大小和时间窗口大小。在下一个例子中,我们将主题设置为具有两个事件的缓冲区。如果广播了更多的事件,只有最后两个事件将被重新传递给订阅者。因此,在这里我们将看不到第一个字符串,因为当订阅它时,我们的主题缓冲区中有B和C。时间窗口也是一样的。我们可以指定主题只缓存在某个时间之前发生的事件,丢弃较旧的事件。因此,在第四个例子中,我们将只看到最后两个事件;较旧的事件不符合时间窗口限制。
AsyncSubject类似于任务并行库中的Task类型。它表示单个异步操作。如果有多个事件被发布,它会等待事件序列完成,并且只向订阅者提供最后一个事件。
BehaviorSubject与ReplaySubject类型非常相似,但它只缓存一个值,并允许在我们尚未发送任何通知的情况下指定默认值。在我们的最后一个例子中,我们将看到所有的字符串被打印出来,因为我们提供了一个默认值,并且所有其他事件都发生在订阅之后。如果我们将behaviorSubject.OnNext("B");行向上移动到Default事件下面,它将替换输出中的默认值。
创建一个 Observable 对象
这个步骤将描述创建Observable对象的不同方法。
准备工作
要按照这个步骤,你需要一个运行中的 Visual Studio 2012。不需要其他先决条件。这个步骤的源代码可以在BookSamples\Chapter8\Recipe4中找到。
如何做到...
要了解创建Observable对象的不同方法,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
添加对Reactive Extensions Main Library NuGet 包的引用。有关如何执行此操作的详细信息,请参考将集合转换为异步 Observable步骤。
-
在
Program.cs文件中,添加以下using指令:
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
- 在
Main方法下方添加以下代码片段:
static IDisposable OutputToConsole<T>(IObservable<T> sequence)
{
return sequence.Subscribe(obj => Console.WriteLine("{0}", obj), ex => Console.WriteLine("Error: {0}", ex.Message), () => Console.WriteLine("Completed"));
}
- 在
Main方法内添加以下代码片段:
IObservable<int> o = Observable.Return(0);
using (var sub = OutputToConsole(o));
Console.WriteLine(" ---------------- ");
o = Observable.Empty<int>();
using (var sub = OutputToConsole(o));
Console.WriteLine(" ---------------- ");
o = Observable.Throw<int>(new Exception());
using (var sub = OutputToConsole(o));
Console.WriteLine(" ---------------- ");
o = Observable.Repeat(42);
using (var sub = OutputToConsole(o.Take(5)));
Console.WriteLine(" ---------------- ");
o = Observable.Range(0, 10);
using (var sub = OutputToConsole(o));
Console.WriteLine(" ---------------- ");
o = Observable.Create<int>(ob => {
for (int i = 0; i < 10; i++)
{
ob.OnNext(i);
}
return Disposable.Empty;
});
using (var sub = OutputToConsole(o)) ;
Console.WriteLine(" ---------------- ");
o = Observable.Generate(0 // initial state, i => i < 5 // while this is true we continue the sequence, i => ++i // iteration, i => i*2 // selecting result);
using (var sub = OutputToConsole(o));
Console.WriteLine(" ---------------- ");
IObservable<long> ol = Observable.Interval(TimeSpan.FromSeconds(1));
using (var sub = OutputToConsole(ol))
{
Thread.Sleep(TimeSpan.FromSeconds(3));
};
Console.WriteLine(" ---------------- ");
ol = Observable.Timer(DateTimeOffset.Now.AddSeconds(2));
using (var sub = OutputToConsole(ol))
{
Thread.Sleep(TimeSpan.FromSeconds(3));
};
Console.WriteLine(" ---------------- ");
- 运行程序。
工作原理...
在这里,我们将介绍创建observables的不同场景。大部分这些功能都是Observable类型的静态工厂方法提供的。前两个示例展示了如何创建一个产生单个值的Observable方法和一个不产生值的方法。在下一个示例中,我们使用Observable.Throw来构造一个触发其观察者的OnError处理程序的Observable类。
Observable.Repeat方法表示一个无限序列。这个方法有不同的重载;在这里,我们通过重复 42 个值来构造一个无限序列。然后我们使用 LINQ 的Take方法从这个序列中取出五个元素。Observable.Range表示一个值的范围,就像Enumerable.Range一样。
Observable.Create方法支持更多的自定义场景。有很多重载允许我们使用取消标记和任务,但让我们看看最简单的一个。它接受一个函数,该函数接受一个观察者实例,并返回一个表示订阅的IDisposable对象。如果我们有任何需要清理的资源,我们可以在这里提供清理逻辑,但我们只返回一个空的可处置对象,因为实际上我们并不需要它。
Observable.Generate是创建自定义序列的另一种方法。我们必须为序列提供一个初始值,然后提供一个确定是否应生成更多项或完成序列的谓词。然后我们提供一个迭代逻辑,在我们的情况下是递增计数器。最后一个参数是一个选择器函数,允许我们自定义结果。
最后两种方法处理定时器。Observable.Interval开始生成具有TimeSpan周期的定时器滴答事件,而Observable.Timer也指定了启动时间。
针对可观察集合使用 LINQ 查询
这个配方展示了如何使用 LINQ 来查询异步事件序列。
准备就绪
要按照这个示例,您需要 Visual Studio 2012。不需要其他先决条件。此示例的源代码可以在BookSamples\Chapter8\Recipe5中找到。
如何做...
要理解针对可观察集合使用 LINQ 查询,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
添加对Reactive Extensions Main Library NuGet 包的引用。有关如何执行此操作的详细信息,请参阅将集合转换为异步可观察配方。
-
在
Program.cs文件中,添加以下using指令:
using System;
using System.Reactive.Linq;
- 在
Main方法下面添加以下代码片段:
static IDisposable OutputToConsole<T>(IObservable<T> sequence, int innerLevel)
{
string delimiter = innerLevel == 0 ? string.Empty : new string('-', innerLevel*3);
return sequence.Subscribe(obj => Console.WriteLine("{0}{1}", delimiter, obj), ex => Console.WriteLine("Error: {0}", ex.Message), () => Console.WriteLine("{0}Completed", delimiter));
}
- 在
Main方法内部添加以下代码片段:
IObservable<long> sequence = Observable.Interval(TimeSpan.FromMilliseconds(50)).Take(21);
var evenNumbers = from n in sequencewhere n % 2 == 0select n;
var oddNumbers = from n in sequencewhere n % 2 != 0select n;
var combine = from n in evenNumbers.Concat(oddNumbers)select n;
var nums = (from n in combinewhere n % 5 == 0select n).Do(n => Console.WriteLine("------Number {0} is processed in Do method", n));
using (var sub = OutputToConsole(sequence, 0))
using (var sub2 = OutputToConsole(combine, 1))
using (var sub3 = OutputToConsole(nums, 2))
{
Console.WriteLine("Press enter to finish the demo");
Console.ReadLine();
}
- 运行程序。
它是如何工作的...
针对Observable事件序列使用 LINQ 的能力是 Reactive Extensions 框架的主要优势。有许多不同的有用场景;不幸的是,这里不可能展示所有这些场景。我尝试提供一个简单但非常有说明性的示例,它没有太多复杂的细节,展示了当应用于异步可观察集合时,LINQ 查询的工作原理的本质。
首先,我们创建一个Observable事件,每 50 毫秒生成一个数字序列,从零开始,取其中的 21 个事件。然后,我们对这个序列进行 LINQ 查询。首先,我们只选择序列中的偶数,然后只选择奇数,然后我们连接这两个序列。
最终的查询显示了如何使用非常有用的方法Do,它允许引入副作用,例如记录结果序列中的每个值。为了运行所有查询,我们创建了嵌套的订阅,因为序列最初是异步的,所以我们必须非常小心地处理订阅的生命周期。外部范围表示对定时器的订阅,内部订阅处理组合序列查询和副作用查询。如果我们过早按Enter键,我们只需取消订阅定时器,从而停止演示。
当我们运行演示时,我们可以看到不同查询如何实时交互的实际过程。我们可以看到我们的查询是惰性的,它们只有在我们订阅它们的结果时才开始运行。定时器事件序列打印在第一列中。当偶数查询得到偶数时,它也打印出来,使用---前缀来区分这个序列结果和第一个序列结果。最终的查询结果打印到右列中。
当程序运行时,我们可以看到定时器序列、偶数序列和副作用序列并行运行。只有连接等待偶数序列完成。如果我们不连接这些序列,我们将有四个并行事件序列相互交互的最有效方式!这显示了 Reactive Extensions 的真正力量,并且可能是深入学习这个库的良好起点。
创建具有 Rx 的异步操作
这个配方展示了如何从其他编程模式中定义的异步操作中创建Observable。
准备就绪
要按照此示例操作,您需要 Visual Studio 2012。不需要其他先决条件。此示例的源代码可以在BookSamples\Chapter8\Recipe6中找到。
如何操作...
要了解如何使用 Rx 创建异步操作,请执行以下步骤:
-
开始 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
将对Reactive Extensions Main Library NuGet 包添加引用。有关如何执行此操作的详细信息,请参阅将集合转换为异步可观察对象的示例。
-
在
Program.cs文件中,添加以下using指令:
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Timer = System.Timers.Timer;
- 在
Main方法下面添加以下代码片段:
static async Task<T> AwaitOnObservable<T>(IObservable<T> observable)
{
T obj = await observable;
Console.WriteLine("{0}", obj );
return obj;
}
static Task<string> LongRunningOperationTaskAsync(string name)
{
return Task.Run(() => LongRunningOperation(name));
}
static IObservable<string> LongRunningOperationAsync(string name)
{
return Observable.Start(() => LongRunningOperation(name));
}
static string LongRunningOperation(string name)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
return string.Format("Task {0} is completed. Thread Id {1}", name, Thread.CurrentThread.ManagedThreadId);
}
static IDisposable OutputToConsole(IObservable<EventPattern<ElapsedEventArgs>> sequence)
{
return sequence.Subscribe(obj => Console.WriteLine("{0}", obj.EventArgs.SignalTime), ex => Console.WriteLine("Error: {0}", ex.Message), () => Console.WriteLine("Completed"));
}
static IDisposable OutputToConsole<T>(IObservable<T> sequence)
{
return sequence.Subscribe(
obj => Console.WriteLine("{0}", obj), ex => Console.WriteLine("Error: {0}", ex.Message), () => Console.WriteLine("Completed"));
}
- 在
Main方法中添加以下代码片段:
IObservable<string> o = LongRunningOperationAsync("Task1");
using (var sub = OutputToConsole(o))
{
Thread.Sleep(TimeSpan.FromSeconds(2));
};
Console.WriteLine(" ---------------- ");
Task<string> t = LongRunningOperationTaskAsync("Task2");
using (var sub = OutputToConsole(t.ToObservable()))
{
Thread.Sleep(TimeSpan.FromSeconds(2));
};
Console.WriteLine(" ---------------- ");
AsyncDelegate asyncMethod = LongRunningOperation;
// marked as obsolete, use tasks instead
Func<string, IObservable<string>> observableFactory = Observable.FromAsyncPattern<string, string>(asyncMethod.BeginInvoke, asyncMethod.EndInvoke);
o = observableFactory("Task3");
using (var sub = OutputToConsole(o))
{
Thread.Sleep(TimeSpan.FromSeconds(2));
};
Console.WriteLine(" ---------------- ");
o = observableFactory("Task4");
AwaitOnObservable(o).Wait();
Console.WriteLine(" ---------------- ");
using (var timer = new Timer(1000))
{
var ot = Observable.FromEventPattern<ElapsedEventHandler, ElapsedEventArgs>(h => timer.Elapsed += h,h => timer.Elapsed -= h);
timer.Start();
using (var sub = OutputToConsole(ot))
{
Thread.Sleep(TimeSpan.FromSeconds(5));
}
Console.WriteLine(" ---------------- ");
timer.Stop();
}
- 运行程序。
它是如何工作的...
此示例说明了如何将不同类型的异步操作转换为Observable类。步骤 5 中的第一个代码片段使用了Observable.Start方法,这与 TPL 中的Task.Run非常相似。它启动一个给出字符串结果然后完成的异步操作。
注意
我强烈建议使用任务并行库进行异步操作。Reactive Extensions 也支持这种情况,但为了避免歧义,最好在单独的异步操作时坚持使用任务,并且只有在需要处理事件序列时才使用 Rx。另一个建议是将每种类型的单独异步操作转换为任务,然后只有在需要时将任务转换为observable类。
然后,我们使用任务做同样的事情,并通过简单调用ToObservable扩展方法将任务转换为Observable方法。步骤 5 中显示的下一个代码片段是关于将异步编程模型模式转换为Observable。通常,您会将 APM 转换为任务,然后将任务转换为Observable。但是,这里有一个直接的转换,这个示例说明了如何运行一个异步委托并将其包装成Observable操作。
步骤 5 中代码片段的下一部分显示我们能够await一个Observable操作。由于我们无法在Main等入口方法上使用async修饰符,因此我们引入一个返回任务并等待结果任务完成的单独方法到Main方法中。
步骤 5 中代码片段的最后部分是相同的,但现在我们直接将基于事件的异步模式转换为Observable类。我们创建一个计时器,并在 5 秒内使用其事件。然后我们释放计时器以清理资源。
第九章:使用异步 I/O
在本章中,我们将详细讨论异步输入/输出操作。您将学到以下内容:
-
异步处理文件
-
编写异步 HTTP 服务器和客户端
-
异步处理数据库
-
异步调用 WCF 服务
介绍
在之前的章节中,我们已经讨论了正确使用异步输入/输出操作的重要性。为什么这么重要呢?为了有一个坚实的理解,让我们考虑两种应用程序。
如果我们在客户端上运行应用程序,最重要的事情之一就是拥有一个响应迅速的用户界面。这意味着无论应用程序发生什么,所有用户界面元素,如按钮和进度条,都能快速运行,并且用户能够立即得到应用程序的反应。这并不容易实现!如果您尝试在 Windows 中打开记事本文本编辑器,并尝试加载一个几兆字节大小的文档,应用程序窗口将会在相当长的时间内冻结,因为整个文本首先要从磁盘加载,然后程序才开始处理用户输入。
这是一个非常重要的问题,在这种情况下,唯一的解决方案是尽一切可能避免阻塞 UI 线程。这反过来意味着为了防止阻塞 UI 线程,每个与 UI 相关的 API 都必须只允许异步调用。这是 Windows 8 操作系统重新设计 API 的关键原因,几乎用异步模拟替换了几乎每个方法。但是,如果我们的应用程序使用多个线程来实现这个目标,会影响性能吗?当然会!然而,考虑到我们只有一个用户,我们可以付出代价。如果应用程序能够利用计算机的所有能力,以更有效的方式为运行应用程序的单个用户提供服务,那就很好。
让我们再看看第二种情况。如果我们在服务器上运行应用程序,情况就完全不同了。我们把可扩展性作为首要任务,这意味着单个用户应尽可能少地消耗资源。如果我们为每个用户创建许多线程,那么我们就无法很好地扩展。在有效的方式中平衡应用程序资源消耗是一个非常复杂的问题。例如,在微软的 Web 应用程序平台 ASP.NET 中,我们使用一个工作线程池来为客户端请求提供服务。这个池有一定数量的工作线程,我们必须尽量减少每个工作线程的使用时间以实现可扩展性。这意味着我们必须尽快将其返回到池中,以便它可以为另一个请求提供服务。如果我们开始一个需要计算的异步操作,我们将会有一个非常低效的工作流程。首先我们从线程池中取出一个工作线程来为客户端请求提供服务。然后我们再取出另一个工作线程并在其上启动一个异步操作。现在我们有两个工作线程为我们的请求提供服务,如果第一个线程正在做一些有用的事情,那就很好了!不幸的是,通常情况是我们只是等待异步操作完成,我们消耗了两个工作线程而不是一个。在这种情况下,异步实际上比同步执行更糟糕!我们不需要加载所有的 CPU 核心,因为我们已经为许多客户端提供服务,因此正在使用所有的 CPU 计算能力。我们不需要保持第一个线程响应,因为我们没有用户界面。那么为什么我们要在服务器应用程序中使用异步呢?
答案是,当存在异步输入/输出操作时,我们应该使用异步处理。如今,现代计算机通常具有存储文件的硬盘驱动器和通过网络发送和接收数据的网络卡。这两个设备都有自己的微型计算机,用于在非常低的级别上管理输入/输出操作并向操作系统发出结果。这又是一个相当复杂的话题;但为了保持概念清晰,我们可以说程序员有一种方式来启动输入/输出操作,并在操作完成时向操作系统提供一个回调代码。在启动 I/O 任务和其完成之间,不涉及 CPU 工作;它是在相应的磁盘和网络控制器微型计算机中完成的。这种执行 I/O 任务的方式称为 I/O 线程;它们是使用.NET 线程池实现的,并且反过来使用操作系统的 I/O 完成端口基础设施。
在 ASP.NET 中,一旦从工作线程启动了异步 I/O 操作,它就可以立即返回到线程池!在操作进行时,此线程可以为其他客户端提供服务。最后,当操作完成时,ASP.NET 基础结构会从线程池中获取一个空闲的工作线程(可能与启动操作的线程不同),并完成操作。
好了,现在我们明白了 I/O 线程对服务器应用程序有多么重要。不幸的是,很难看出任何给定的 API 是否在后台使用 I/O 线程。除了研究源代码之外,唯一的方法就是知道.NET Framework 类库利用了 I/O 线程。在本章中,我们将看到如何使用其中一些 API。我们将学习如何异步处理文件,如何使用网络 I/O 创建 HTTP 服务器并调用 Windows Communication Foundation 服务,以及如何使用异步 API 查询数据库。
注意
另一个需要考虑的重要问题是并行性。由于许多原因,密集的并行磁盘操作可能会导致性能非常差。请注意,并行 I/O 操作通常非常低效,可能会合理地按顺序但是以异步方式处理 I/O。
异步处理文件
本教程将指导我们如何创建文件,以及如何异步读取和写入数据。
准备工作
要按照本教程,您需要 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在BookSamples\Chapter9\Recipe1中找到。
如何做...
要了解如何异步处理文件,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
在
Program.cs文件中添加以下using指令:
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
- 在
Main方法下面添加以下代码片段:
const int BUFFER_SIZE = 4096;
async static Task ProcessAsynchronousIO()
{
using (var stream = new FileStream("test1.txt", FileMode.Create, FileAccess.ReadWrite, FileShare.None, BUFFER_SIZE))
{
Console.WriteLine("1\. Uses I/O Threads: {0}", stream.IsAsync);
byte[] buffer = Encoding.UTF8.GetBytes(CreateFileContent());
var writeTask = Task.Factory.FromAsync(stream.BeginWrite, stream.EndWrite, buffer, 0, buffer.Length, null);
await writeTask;
}
using (var stream = new FileStream("test2.txt", FileMode.Create, FileAccess.ReadWrite,FileShare.None, BUFFER_SIZE, FileOptions.Asynchronous))
{
Console.WriteLine("2\. Uses I/O Threads: {0}", stream.IsAsync);
byte[] buffer = Encoding.UTF8.GetBytes(CreateFileContent());
var writeTask = Task.Factory.FromAsync(stream.BeginWrite, stream.EndWrite, buffer, 0, buffer.Length, null);
await writeTask;
}
using (var stream = File.Create("test3.txt", BUFFER_SIZE, FileOptions.Asynchronous))
using (var sw = new StreamWriter(stream))
{
Console.WriteLine("3\. Uses I/O Threads: {0}", stream.IsAsync);
await sw.WriteAsync(CreateFileContent());
}
using (var sw = new StreamWriter("test4.txt", true))
{
Console.WriteLine("4\. Uses I/O Threads: {0}", ((FileStream)sw.BaseStream).IsAsync);
await sw.WriteAsync(CreateFileContent());
}
Console.WriteLine("Starting parsing files in parallel");
Task<long>[] readTasks = new Task<long>[4];
for (int i = 0; i < 4; i++)
{
readTasks[i] = SumFileContent(string.Format("test{0}.txt", i + 1));
}
long[] sums = await Task.WhenAll(readTasks);
Console.WriteLine("Sum in all files: {0}", sums.Sum());
Console.WriteLine("Deleting files...");
Task[] deleteTasks = new Task[4];
for (int i = 0; i < 4; i++)
{
string fileName = string.Format("test{0}.txt", i + 1);
deleteTasks[i] = SimulateAsynchronousDelete(fileName);
}
await Task.WhenAll(deleteTasks);
Console.WriteLine("Deleting complete.");
}
static string CreateFileContent()
{
var sb = new StringBuilder();
for (int i = 0; i < 100000; i++)
{
sb.AppendFormat("{0}", new Random(i).Next(0, 99999));
sb.AppendLine();
}
return sb.ToString();
}
async static Task<long> SumFileContent(string fileName)
{
using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read,FileShare.None, BUFFER_SIZE, FileOptions.Asynchronous))
using (var sr = new StreamReader(stream))
{
long sum = 0;
while (sr.Peek() > -1)
{
string line = await sr.ReadLineAsync();
sum += long.Parse(line);
}
return sum;
}
}
static Task SimulateAsynchronousDelete(string fileName)
{
return Task.Run(() => File.Delete(fileName));
}
- 在
Main方法中添加以下代码片段:
var t = ProcessAsynchronousIO();
t.GetAwaiter().GetResult();
- 运行程序。
工作原理...
程序运行时,我们以不同的方式创建四个文件,并用随机数据填充它们。在第一种情况下,我们使用FileStream类及其方法,将异步编程模型 API 转换为任务;在第二种情况下,我们做同样的事情,但是我们为FileStream构造函数提供了FileOptions.Asynchronous。
注意
非常重要的是使用FileOptions.Asynchronous选项。如果我们省略此选项,仍然可以以异步方式处理文件,但这只是线程池上的异步委托调用!只有在提供此选项(或另一个构造函数重载中的bool useAsync)时,我们才能使用FileStream类进行 I/O 异步处理。
第三种情况使用了一些简化的 API,比如File.Create方法和StreamWriter类。它仍然使用 I/O 线程,我们可以通过使用stream.IsAsync属性来检查。最后一种情况说明了过度简化也是不好的。在这里,我们没有利用 I/O 的异步性,而是通过异步委托调用来模拟它。
现在我们从文件中进行并行异步读取,对它们的内容进行求和,然后再将它们相加。最后,我们删除所有的文件。由于在任何非 Windows 存储应用程序中都没有异步删除文件的方法,我们使用Task.Run工厂方法来模拟异步。
编写一个异步 HTTP 服务器和客户端
这个步骤展示了如何创建一个简单的异步 HTTP 服务器。
准备工作
要按照这个步骤,你需要 Visual Studio 2012。不需要其他先决条件。这个步骤的源代码可以在BookSamples\Chapter9\Recipe2中找到。
如何做...
以下步骤演示了如何创建一个简单的异步 HTTP 服务器:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
添加对
System.Net.Http框架库的引用。 -
在
Program.cs文件中添加以下using指令:
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
- 在
Main方法下面添加以下代码片段:
static async Task GetResponseAsync(string url)
{
using (var client = new HttpClient())
{
HttpResponseMessage responseMessage = await client.GetAsync(url);
string responseHeaders = responseMessage.Headers.ToString();
string response = await responseMessage.Content.ReadAsStringAsync();
Console.WriteLine("Response headers:");
Console.WriteLine(responseHeaders);
Console.WriteLine("Response body:");
Console.WriteLine(response);
}
}
class AsyncHttpServer
{
readonly HttpListener _listener;
const string RESPONSE_TEMPLATE = "<html><head><title>Test</title></head><body><h2>Test page</h2><h4>Today is: {0}</h4></body></html>";
public AsyncHttpServer(int portNumber)
{
_listener = new HttpListener();
_listener.Prefixes.Add(string.Format("http://+:{0}/", portNumber));
}
public async Task Start()
{
_listener.Start();
while (true)
{
var ctx = await _listener.GetContextAsync();
Console.WriteLine("Client connected...");
string response = string.Format(RESPONSE_TEMPLATE, DateTime.Now);
using (var sw = new StreamWriter(ctx.Response.OutputStream))
{
await sw.WriteAsync(response);
await sw.FlushAsync();
}
}
}
public async Task Stop()
{
_listener.Abort();
}
}
- 在
Main方法中添加以下代码片段:
var server = new AsyncHttpServer(portNumber: 1234);
var t = Task.Run(() => server.Start());
Console.WriteLine("Listening on port 1234\. Open http://localhost:1234 in your browser.");
Console.WriteLine("Trying to connect:");
Console.WriteLine();
GetResponseAsync("http://localhost:1234").GetAwaiter().GetResult();
Console.WriteLine();
Console.WriteLine("Press Enter to stop the server.");
Console.ReadLine();
server.Stop().GetAwaiter().GetResult();
- 运行程序。
工作原理...
在这里,我们使用HttpListener类实现了一个非常简单的 Web 服务器。还有一个TcpListener类用于 TCP 套接字 I/O 操作。我们配置我们的监听器以接受来自本地机器上任何主机的连接,端口为1234。然后我们在一个单独的工作线程中启动监听器,这样我们就可以从主线程中控制它。
当我们使用GetContextAsync方法时,异步 I/O 操作发生。不幸的是,它不接受CancellationToken用于取消场景;所以当我们想要停止服务器时,我们只需调用_listener.Abort方法,它会放弃所有连接并停止服务器。
要对这个服务器执行异步请求,我们使用System.Net.Http程序集中的HttpClient类和相同的命名空间。我们使用GetAsync方法来发出异步的 HTTP GET请求。还有其他 HTTP 请求的方法,比如POST、DELETE和PUT。HttpClient还有许多其他选项,比如使用不同格式(如 XML 和 JSON)对对象进行序列化和反序列化,指定代理服务器地址、凭据等。
当你运行程序时,你会看到服务器已经启动。在服务器代码中,我们使用GetContextAsync方法来接受新的客户端连接。当一个新的客户端连接时,这个方法会返回,我们只是简单地输出一个包含当前日期和时间的基本 HTML 到响应中。然后我们请求服务器并打印响应头和内容。你也可以打开浏览器并浏览到http://localhost:1234/的 URL。你会在浏览器窗口中看到相同的响应。
异步处理数据库
这个步骤将指导我们创建一个数据库,用数据填充它,并异步读取数据的过程。
准备工作
要按照这个步骤,你需要运行 Visual Studio 2012。不需要其他先决条件。这个步骤的源代码可以在BookSamples\Chapter9\Recipe3中找到。
如何做...
为了理解创建数据库、填充数据和异步读取数据的过程,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs文件中添加以下using指令:
using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
- 在
Main方法下面添加以下代码片段:
async static Task ProcessAsynchronousIO(string dbName)
{
try
{
const string connectionString = @"Data Source=(LocalDB)\v11.0;Initial Catalog=master;Integrated Security=True";
string outputFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string dbFileName = Path.Combine(outputFolder, string.Format(@".\{0}.mdf", dbName));
string dbLogFileName = Path.Combine(outputFolder, string.Format(@".\{0}_log.ldf", dbName));
string dbConnectionString = string.Format(@"Data Source=(LocalDB)\v11.0;AttachDBFileName={1};Initial Catalog={0};Integrated Security=True;", dbName, dbFileName);
using (var connection = new SqlConnection(connectionString))
{
await connection.OpenAsync();
if (File.Exists(dbFileName))
{
Console.WriteLine("Detaching the database...");
var detachCommand = new SqlCommand("sp_detach_db", connection);
detachCommand.CommandType = CommandType.StoredProcedure;
detachCommand.Parameters.AddWithValue("@dbname", dbName);
await detachCommand.ExecuteNonQueryAsync();
Console.WriteLine("The database was detached successfully.");
Console.WriteLine("Deleting the database...");
if(File.Exists(dbLogFileName)) File.Delete(dbLogFileName);
File.Delete(dbFileName);
Console.WriteLine("The database was deleted successfully.");
}
Console.WriteLine("Creating the database...");
string createCommand = String.Format("CREATE DATABASE {0} ON (NAME = N'{0}', FILENAME = '{1}')", dbName, dbFileName);
var cmd = new SqlCommand(createCommand, connection);
await cmd.ExecuteNonQueryAsync();
Console.WriteLine("The database was created successfully");
}
using (var connection = new SqlConnection(dbConnectionString))
{
await connection.OpenAsync();
var cmd = new SqlCommand("SELECT newid()", connection);
var result = await cmd.ExecuteScalarAsync();
Console.WriteLine("New GUID from DataBase: {0}", result);
cmd = new SqlCommand(@"CREATE TABLE [dbo].CustomTable NOT NULL, [Name] nvarchar NOT NULL,CONSTRAINT [PK_ID] PRIMARY KEY CLUSTERED ([ID] ASC) ON [PRIMARY]) ON [PRIMARY]", connection);
await cmd.ExecuteNonQueryAsync();
Console.WriteLine("Table was created successfully.");
cmd = new SqlCommand(@"INSERT INTO [dbo].[CustomTable] (Name) VALUES ('John');
INSERT INTO [dbo].[CustomTable] (Name) VALUES ('Peter');
INSERT INTO [dbo].[CustomTable] (Name) VALUES ('James');
INSERT INTO [dbo].[CustomTable] (Name) VALUES ('Eugene');", connection);
await cmd.ExecuteNonQueryAsync();
Console.WriteLine("Inserted data successfully ");
Console.WriteLine("Reading data from table...");
cmd = new SqlCommand(@"SELECT * FROM [dbo].[CustomTable]", connection);
using (SqlDataReader reader = await cmd.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
var id = reader.GetFieldValue<int>(0);
var name = reader.GetFieldValue<string>(1);
Console.WriteLine("Table row: Id {0}, Name {1}", id, name);
}
}
}
}
catch(Exception ex)
{
Console.WriteLine("Error: {0}", ex.Message);
}
}
- 在
Main方法中添加以下代码片段:
const string dataBaseName = "CustomDatabase";
var t = ProcessAsynchronousIO(dataBaseName);
t.GetAwaiter().GetResult();
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
- 运行程序。
工作原理...
这个程序使用一个名为 SQL Server 2012 LocalDb 的软件。它与 Visual Studio 2012 一起安装,应该可以正常工作。但是,如果出现错误,您可能需要从安装向导中修复此组件。
我们首先配置到我们的数据库文件的路径。我们将数据库文件放在程序执行文件夹中。将有两个文件:一个用于数据库本身,另一个用于事务日志文件。我们还配置了两个连接字符串,定义了我们如何连接到我们的数据库。第一个是连接到 LocalDb 引擎以分离我们的数据库;如果它已经存在,则删除然后重新创建它。我们利用 I/O 异步性来打开连接,并使用OpenAsync和ExecuteNonQueryAsync方法分别执行 SQL 命令。
完成此任务后,我们将附加一个新创建的数据库。在这里,我们创建一个新表并向其中插入一些数据。除了前面提到的方法之外,我们使用ExecuteScalarAsync来异步从数据库引擎获取标量值,并使用SqlDataReader.ReadAsync方法来异步从数据库表中读取数据行。
如果我们的数据库中有一个包含大型二进制值的大表,那么我们将使用CommandBehavior.SequentialAcess枚举来创建数据读取器,并使用GetFieldValueAsync方法来异步从读取器中获取大字段值。
异步调用 WCF 服务
本教程将描述如何创建一个 WCF 服务,在控制台应用程序中托管它,使服务元数据可用于客户端,并以异步方式消费它。
准备工作
要按照本教程进行步骤,您需要运行 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在BookSamples\Chapter9\Recipe4中找到。
如何做...
要了解如何使用 WCF 服务,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
添加对
System.ServiceModel库的引用。在项目中右键单击引用文件夹,然后选择**添加引用...**菜单选项。添加对System.ServiceModel库的引用。您可以使用引用管理器对话框中的搜索功能,如下截图所示: -
在
Program.cs文件中添加以下using指令:
using System;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Threading.Tasks;
- 在
Main方法下面添加以下代码片段:
const string SERVICE_URL = "http://localhost:1234/HelloWorld";
static async Task RunServiceClient()
{
var endpoint = new EndpointAddress(SERVICE_URL);
var channel = ChannelFactory<IHelloWorldServiceClient>.CreateChannel(new BasicHttpBinding(), endpoint);
var greeting = await channel.GreetAsync("Eugene");
Console.WriteLine(greeting);
}
[ServiceContract(Namespace = "Packt", Name = "HelloWorldServiceContract")]
public interface IHelloWorldService
{
[OperationContract]
string Greet(string name);
}
[ServiceContract(Namespace = "Packt", Name = "HelloWorldServiceContract")]
public interface IHelloWorldServiceClient
{
[OperationContract]string Greet(string name);
[OperationContract]Task<string> GreetAsync(string name);
}
public class HelloWorldService : IHelloWorldService
{
public string Greet(string name)
{
return string.Format("Greetings, {0}!", name);
}
}
- 在
Main方法中添加以下代码片段:
ServiceHost host = null;
try
{
host = new ServiceHost(typeof (HelloWorldService), new Uri(SERVICE_URL));
var metadata = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (null == metadata)
{
metadata = new ServiceMetadataBehavior();
}
metadata.HttpGetEnabled = true;
metadata.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
host.Description.Behaviors.Add(metadata);
host.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName, MetadataExchangeBindings.CreateMexHttpBinding(),"mex");
var endpoint = host.AddServiceEndpoint(typeof (IHelloWorldService), new BasicHttpBinding(), SERVICE_URL);
host.Faulted += (sender, e) => Console.WriteLine("Error!");
host.Open();
Console.WriteLine("Greeting service is running and listening on:");
Console.WriteLine("{0} ({1})", endpoint.Address, endpoint.Binding.Name);
var client = RunServiceClient();
client.GetAwaiter().GetResult();
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("Error in catch block: {0}", ex);
}
finally
{
if (null != host)
{
if (host.State == CommunicationState.Faulted)
{
host.Abort();
}
else
{
host.Close();
}
}
}
- 运行程序。
工作原理...
Windows Communication Foundation 或 WCF 是一个框架,允许我们以不同的方式调用远程服务。其中一种曾经非常流行的方式是使用基于 XML 的协议简单对象访问协议(SOAP)通过 HTTP 调用远程服务。当服务器应用程序调用另一个远程服务时,这是相当常见的,也可以使用 I/O 线程来完成。
Visual Studio 2012 对 WCF 服务有很好的支持;例如,您可以使用添加服务引用菜单选项添加对这些服务的引用。您也可以对我们的服务进行此操作,因为我们提供了服务元数据。
创建这样一个服务,我们需要使用ServiceHost类来托管我们的服务。我们通过提供服务实现类型和服务的基本 URI 来描述我们将托管的服务。然后我们配置元数据端点和服务端点。最后,我们处理Faulted事件以处理错误,并运行主机服务。
为了消费这个服务,我们创建一个客户端,这就是主要的技巧所在。在服务器端,我们有一个带有通常同步方法的服务,称为Greet。这个方法在服务契约IHelloWorldService中定义。然而,如果我们想利用异步网络 I/O,我们必须异步调用这个方法。我们可以通过创建一个新的服务契约来做到这一点,其中包含匹配的命名空间和服务名称,在这里我们定义同步和基于任务的异步方法。尽管我们在服务器端没有异步方法的定义,但我们遵循命名约定,WCF 基础设施会理解我们想要创建一个异步代理方法。
因此,当我们创建一个IHelloWorldServiceClient代理通道时,WCF 会正确地将异步调用路由到服务器端的同步方法。如果您让应用程序保持运行状态,您可以打开浏览器并使用其 URL 访问服务,即http://localhost:1234/HelloWorld。将打开一个服务描述,您可以浏览到允许我们从 Visual Studio 2012 添加服务引用的 XML 元数据。如果您尝试生成引用,您将看到稍微复杂一些的代码,但它是自动生成的并且易于使用。
第十章:并行编程模式
在本章中,我们将回顾程序员在尝试实现并行工作流时经常面临的常见问题。您将学习以下内容:
-
实现延迟共享状态
-
使用 BlockingCollection 实现并行管道
-
使用 TPL DataFlow 实现并行管道
-
使用 PLINQ 实现 Map/Reduce
介绍
编程中的模式意味着针对特定问题的具体和标准解决方案。通常,编程模式是人们积累经验、分析常见问题并提供解决方案的结果。
由于并行编程已经存在了很长时间,因此有许多不同的模式用于编程并行应用程序。甚至有专门的编程语言来使特定并行算法的编程更容易。然而,事情开始变得越来越复杂。在本书中,我将提供一个起点,让您能够进一步学习并行编程。我们将回顾一些非常基本但非常有用的模式,这些模式对并行编程中的许多常见情况非常有帮助。
首先是关于从多个线程使用共享状态对象。我想强调的是,尽量避免这样做。正如我们在之前的章节中讨论过的,当您编写并行算法时,共享状态真的很糟糕,但在许多情况下是不可避免的。我们将找出如何延迟对象的实际计算直到需要它,并且如何实现不同的场景以实现线程安全。
接下来的两个示例将展示如何创建结构化的并行数据流。我们将回顾一个生产者/消费者模式的具体案例,称为并行管道。我们将首先通过阻塞集合来实现它,然后看看微软为并行编程提供的另一个库TPL DataFlow有多么有用。
我们将学习的最后一个模式是Map/Reduce模式。在现代世界中,这个名字可能意味着非常不同的东西。有些人认为 map/reduce 不是解决任何问题的常见方法,而是大型分布式集群计算的具体实现。我们将找出这个模式背后的含义,并回顾一些例子,说明它在小型并行应用程序的情况下如何工作。
实现延迟共享状态
这个示例展示了如何编写一个延迟共享状态对象。
准备工作
要开始使用这个示例,您需要运行 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples的Chapter10\Recipe1中找到。
如何做...
要实现延迟共享状态,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs文件中,添加以下using指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main方法下面添加以下代码片段:
static async Task ProcessAsynchronously()
{
var unsafeState = new UnsafeState();
Task[] tasks = new Task[4];
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(unsafeState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
var firstState = new DoubleCheckedLocking();
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(firstState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
var secondState = new BCLDoubleChecked();
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(secondState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
var thirdState = new Lazy<ValueToAccess>(Compute);
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(thirdState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
var fourthState = new BCLThreadSafeFactory();
for (int i = 0; i < 4; i++)
{
tasks[i] = Task.Run(() => Worker(fourthState));
}
await Task.WhenAll(tasks);
Console.WriteLine(" --------------------------- ");
}
static void Worker(IHasValue state)
{
Console.WriteLine("Worker runs on thread id {0}",Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("State value: {0}", state.Value.Text);
}
static void Worker(Lazy<ValueToAccess> state)
{
Console.WriteLine("Worker runs on thread id {0}",Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("State value: {0}", state.Value.Text);
}
static ValueToAccess Compute()
{
Console.WriteLine("The value is being constructed on athread id {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromSeconds(1));
return new ValueToAccess(string.Format("Constructed on thread id {0}",Thread.CurrentThread.ManagedThreadId));
}
class ValueToAccess
{
private readonly string _text;
public ValueToAccess(string text)
{
_text = text;
}
public string Text
{
get { return _text; }
}
}
class UnsafeState : IHasValue
{
private ValueToAccess _value;
public ValueToAccess Value
{
get
{
if (_value == null)
{
_value = Compute();
}
return _value;
}
}
}
class DoubleCheckedLocking : IHasValue
{
private object _syncRoot = new object();
private volatile ValueToAccess _value;
public ValueToAccess Value
{
get
{
if (_value == null)
{
lock (_syncRoot)
{
if (_value == null) _value = Compute();
}
}
return _value;
}
}
}
class BCLDoubleChecked : IHasValue
{
private object _syncRoot = new object();
private ValueToAccess _value;
private bool _initialized = false;
public ValueToAccess Value
{
get
{
return LazyInitializer.EnsureInitialized(
ref _value, ref _initialized, ref _syncRoot,Compute);
}
}
}
class BCLThreadSafeFactory : IHasValue
{
private ValueToAccess _value;
public ValueToAccess Value
{
get
{
return LazyInitializer.EnsureInitialized(ref _value,Compute);
}
}
}
interface IHasValue
{
ValueToAccess Value { get; }
}
- 在
Main方法中添加以下代码片段:
var t = ProcessAsynchronously();
t.GetAwaiter().GetResult();
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
- 运行程序。
工作原理...
第一个示例展示了为什么不能在多个访问线程中使用UnsafeState对象是不安全的。我们看到Construct方法被多次调用,不同的线程使用不同的值,这显然是不正确的。为了解决这个问题,我们可以在读取值时使用锁定,如果它尚未初始化,则首先创建它。这样做是有效的,但是在每次读取操作时使用锁定并不高效。为了避免每次使用锁定,有一种传统的方法叫做双重检查锁定模式。我们首次检查值,如果不为空,我们避免不必要的锁定,直接使用共享对象。然而,如果尚未构造,我们使用锁定,然后第二次检查值,因为在我们的第一次检查和锁定操作之间它可能已经初始化。如果它仍未初始化,那么我们才计算值。我们可以清楚地看到这种方法适用于第二个示例——只有一次对Construct方法的调用,第一个调用的线程定义了共享对象的状态。
注意
请注意,如果延迟评估对象的实现是线程安全的,这并不意味着它的所有属性也是线程安全的。
例如,如果向ValueToAccess对象添加一个int公共属性,它将不是线程安全的;您仍然需要使用交错构造或锁定来确保线程安全。
这种模式非常常见,这就是为什么基类库中有几个类来帮助我们。首先,我们可以使用LazyInitializer.EnsureInitialized方法,它在内部实现了双重检查锁定模式。然而,最舒适的选项是使用Lazy<T>类,它允许我们拥有开箱即用的线程安全的延迟评估、共享状态。接下来的两个示例向我们展示它们等同于第二个示例,程序的行为也是相同的。唯一的区别是,由于LazyInitializer是一个静态类,我们不必像在Lazy<T>的情况下创建一个新的类的实例,因此在某些情况下第一种情况的性能会更好。
最后的选择是完全避免锁定,如果我们不关心Construct方法。如果它是线程安全的,没有副作用和/或严重的性能影响,我们可以多次运行它,但只使用第一次构造的值。最后一个示例展示了所描述的行为,我们可以通过使用另一个LazyInitializer.EnsureInitialized方法重载来实现这个结果。
使用 BlockingCollection 实现并行管道
本篇将描述如何使用标准的BlockingCollection数据结构实现生产者/消费者模式的特定场景,称为并行管道。
准备工作
要开始本篇,您需要运行 Visual Studio 2012。没有其他先决条件。本篇的源代码可以在7644_Code\Chapter10\Recipe2中找到。
操作步骤如下:
要了解如何使用BlockingCollection实现并行管道,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
在
Program.cs文件中,添加以下using指令:
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
- 在
Main方法下面添加以下代码片段:
private const int CollectionsNumber = 4;
private const int Count = 10;
class PipelineWorker<TInput, TOutput>
{
Func<TInput, TOutput> _processor = null;
Action<TInput> _outputProcessor = null;
BlockingCollection<TInput>[] _input;
CancellationToken _token;
public PipelineWorker(
BlockingCollection<TInput>[] input,
Func<TInput, TOutput> processor,
CancellationToken token,
string name)
{
_input = input;
Output = new BlockingCollection<TOutput>[_input.Length];
for (int i = 0; i < Output.Length; i++)
Output[i] = null == input[i] ? null : new BlockingCollection<TOutput>(Count);
_processor = processor;
_token = token;
Name = name;
}
public PipelineWorker(
BlockingCollection<TInput>[] input,
Action<TInput> renderer,
CancellationToken token,
string name)
{
_input = input;
_outputProcessor = renderer;
_token = token;
Name = name;
Output = null;
}
public BlockingCollection<TOutput>[] Output { get; private set; }
public string Name { get; private set; }
public void Run()
{
Console.WriteLine("{0} is running", this.Name);
while (!_input.All(bc => bc.IsCompleted) && !_token.IsCancellationRequested)
{
TInput receivedItem;
int i = BlockingCollection<TInput>.TryTakeFromAny(
_input, out receivedItem, 50, _token);
if (i >= 0)
{
if (Output != null)
{
TOutput outputItem = _processor(receivedItem);
BlockingCollection<TOutput>.AddToAny(Output,outputItem);
Console.WriteLine("{0} sent {1} to next,on thread id {2}", Name, outputItem,Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromMilliseconds(100));
}
else
{
_outputProcessor(receivedItem);
}
}
else
{
Thread.Sleep(TimeSpan.FromMilliseconds(50));
}
}
if (Output != null)
{
foreach (var bc in Output) bc.CompleteAdding();
}
}
}
- 在
Main方法内部添加以下代码片段:
var cts = new CancellationTokenSource();
Task.Run(() =>
{
if (Console.ReadKey().KeyChar == 'c')
cts.Cancel();
});
var sourceArrays = new BlockingCollection<int>[CollectionsNumber];
for (int i = 0; i < sourceArrays.Length; i++)
{
sourceArrays[i] = new BlockingCollection<int>(Count);
}
var filter1 = new PipelineWorker<int, decimal>
(sourceArrays,
(n) => Convert.ToDecimal(n * 0.97),
cts.Token,
"filter1"
);
var filter2 = new PipelineWorker<decimal, string>
(filter1.Output,
(s) => String.Format("--{0}--", s),
cts.Token,
"filter2"
);
var filter3 = new PipelineWorker<string, string>
(filter2.Output,
(s) => Console.WriteLine("The final result is {0} onthread id {1}", s,Thread.CurrentThread.ManagedThreadId), cts.Token,"filter3");
try
{
Parallel.Invoke(
() =>
{
Parallel.For(0, sourceArrays.Length * Count,(j, state) =>
{
if (cts.Token.IsCancellationRequested)
{
state.Stop();
}
int k = BlockingCollection<int>.TryAddToAny(sourceArrays, j);
if (k >= 0)
{
Console.WriteLine("added {0} to source data onthread id {1}", j,Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromMilliseconds(100));
}
});
foreach (var arr in sourceArrays)
{
arr.CompleteAdding();
}
},
() => filter1.Run(),
() => filter2.Run(),
() => filter3.Run()
);
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
Console.WriteLine(ex.Message + ex.StackTrace);
}
if (cts.Token.IsCancellationRequested)
{
Console.WriteLine("Operation has been canceled!Press ENTER to exit.");
}
else
{
Console.WriteLine("Press ENTER to exit.");
}
Console.ReadLine();
- 运行程序。
它是如何工作的...
在前面的示例中,我们实现了最常见的并行编程场景之一。想象一下,我们有一些数据必须通过几个计算阶段,这些阶段需要相当长的时间。后面的计算需要前面的结果,所以我们不能并行运行它们。
如果我们只有一个项目要处理,那么提高性能的可能性就不多。但是,如果我们通过相同的计算阶段运行许多项目,我们可以使用并行管道技术。这意味着我们不必等到所有项目通过第一个计算阶段才进入下一个阶段。只要有一个项目完成了阶段,我们就将其移动到下一个阶段,同时前一个阶段正在处理下一个项目,依此类推。结果几乎是并行处理,只是需要第一个项目通过第一个计算阶段所需的时间。
在这里,我们为每个处理阶段使用了四个集合,说明我们也可以并行处理每个阶段。我们做的第一步是通过按C键提供取消整个过程的可能性。我们创建一个取消令牌并运行一个单独的任务来监视C键。然后,我们定义我们的管道。它由三个主要阶段组成。第一个阶段是我们将初始数字放在作为后续管道的项目来源的前四个集合中。这段代码在Parallel.For循环内,而Parallel.Invoke语句内部,因为我们并行运行所有阶段;初始阶段也是并行运行的。
下一阶段是定义我们的管道元素。逻辑在PipelineWorker类中定义。我们使用输入集合初始化工作程序,提供转换函数,然后并行运行工作程序与其他工作程序。这样我们定义了两个工作程序,或者过滤器,因为它们过滤初始序列。其中一个将整数转换为十进制值,另一个将十进制转换为字符串。最后,最后一个工作程序只是将每个传入的字符串打印到控制台。我们在每个地方都提供了运行线程 ID 以查看一切是如何工作的。除此之外,我们添加了人为的延迟,以便项目处理更加自然,因为我们真的使用了繁重的计算。
结果,我们看到了确切的预期行为。首先,一些项目被创建在初始集合上。然后,我们看到第一个过滤器开始处理它们,随着它们被处理,第二个过滤器开始工作,最后项目进入最后一个工作程序,将其打印到控制台。
使用 TPL DataFlow 实现并行管道
这个教程展示了如何使用 TPL DataFlow 库实现并行管道模式。
准备工作
要开始这个教程,你需要一个运行的 Visual Studio 2012. 没有其他先决条件。这个教程的源代码可以在7644_Code\Chapter10\Recipe3中找到。
如何做...
要了解如何使用 TPL DataFlow 实现并行管道,执行以下步骤:
-
启动 Visual Studio 2012. 创建一个新的 C# 控制台应用程序项目。
-
添加对Microsoft TPL DataFlow NuGet 包的引用。
-
右键单击项目中的References文件夹,选择**管理 NuGet 包...**菜单选项。
-
现在添加你喜欢的Microsoft TPL DataFlow NuGet 包的引用。你可以使用管理 NuGet 包对话框中的搜索选项,如下所示:
- 在
Program.cs文件中,添加以下using指令:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
- 在
Main方法下面添加以下代码片段:
async static Task ProcessAsynchronously()
{
var cts = new CancellationTokenSource();
Task.Run(() =>
{
if (Console.ReadKey().KeyChar == 'c')
cts.Cancel();
});
var inputBlock = new BufferBlock<int>(
new DataflowBlockOptions { BoundedCapacity = 5,CancellationToken = cts.Token });
var filter1Block = new TransformBlock<int, decimal>(
n =>
{
decimal result = Convert.ToDecimal(n * 0.97);
Console.WriteLine("Filter 1 sent {0} to the nextstage on thread id {1}", result,Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromMilliseconds(100));
return result;
},
new ExecutionDataflowBlockOptions {MaxDegreeOfParallelism = 4, CancellationToken =cts.Token });
var filter2Block = new TransformBlock<decimal, string>(
n =>
{
string result = string.Format("--{0}--", n);
Console.WriteLine("Filter 2 sent {0} to the nextstage on thread id {1}", result,Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromMilliseconds(100));
return result;
},
new ExecutionDataflowBlockOptions {
MaxDegreeOfParallelism = 4, CancellationToken =cts.Token });
var outputBlock = new ActionBlock<string>(
s =>
{
Console.WriteLine("The final result is {0} on threadid {1}", s, Thread.CurrentThread.ManagedThreadId);
},
new ExecutionDataflowBlockOptions {
MaxDegreeOfParallelism = 4, CancellationToken =cts.Token });
inputBlock.LinkTo(filter1Block, new DataflowLinkOptions {PropagateCompletion = true });
filter1Block.LinkTo(filter2Block, new DataflowLinkOptions{ PropagateCompletion = true });
filter2Block.LinkTo(outputBlock, new DataflowLinkOptions{ PropagateCompletion = true });
try
{
Parallel.For(0, 20, new ParallelOptions {MaxDegreeOfParallelism = 4, CancellationToken =cts.Token }
, i =>
{
Console.WriteLine("added {0} to source data on threadid {1}", i, Thread.CurrentThread.ManagedThreadId);
inputBlock.SendAsync(i).GetAwaiter().GetResult();
});
inputBlock.Complete();
await outputBlock.Completion;
Console.WriteLine("Press ENTER to exit.");
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation has been canceled!Press ENTER to exit.");
}
Console.ReadLine();
}
- 在
Main方法中添加以下代码片段:
var t = ProcessAsynchronously();
t.GetAwaiter().GetResult();
- 运行程序。
工作原理...
在上一个教程中,我们已经实现了一个并行管道模式,通过顺序阶段处理项目。这是一个很常见的问题,而且编写这样的算法的一种提议的方法是使用微软的 TPL DataFlow 库。它通过NuGet分发,很容易在你的应用程序中安装和使用。
TPL DataFlow 库包含不同类型的块,可以以不同的方式连接在一起,形成可以部分并行和顺序执行的复杂过程。要查看一些可用的基础设施,让我们使用 TPL DataFlow 库来实现前面的场景。
首先,我们定义将处理我们的数据的不同块。请注意,这些块在构建过程中可以指定不同的选项,这些选项可能非常重要。例如,我们将取消标记传递给我们定义的每个块,并且当我们发出取消信号时,所有这些块都将停止工作。
我们从BufferBlock开始我们的过程。这个块保存项目以便将其传递给流中的下一个块。我们将其限制为五个项目的容量,指定BoundedCapacity选项值。这意味着当这个块中有五个项目时,它将停止接受新项目,直到现有项目中的一个传递到下一个块。
下一个块类型是TransformBlock。这个块用于数据转换步骤。在这里,我们定义了两个转换块,其中一个从整数创建十进制数,另一个从十进制值创建一个字符串。对于这个块,有一个MaxDegreeOfParallelism选项,指定最大同时工作线程数。
最后一个块是ActionBlock类型。这个块将在每个传入的项目上运行指定的操作。我们使用这个块将我们的项目打印到控制台上。
现在,我们使用LinkTo方法将这些块连接在一起。在这里,我们有一个简单的顺序数据流,但也可以创建更复杂的方案。在这里,我们还提供了DataflowLinkOptions,其中PropagateCompletion属性设置为true。这意味着当步骤完成时,它将自动传播其结果和异常到下一个阶段。然后我们并行地开始向缓冲块添加项目,当完成添加新项目时,调用块的Complete方法。然后我们等待最后一个块完成。在取消的情况下,我们处理OperationCancelledException并取消整个过程。
使用 PLINQ 实现 Map/Reduce
这个示例将描述如何在使用 PLINQ 时实现Map/Reduce模式。
准备就绪
要开始这个示例,您需要运行 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在7644_Code\Chapter10\Recipe4中找到。
如何做...
要了解如何使用 PLINQ 实现 Map/Reduce,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
在
Program.cs文件中,添加以下using指令:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
- 在
Main方法下面添加以下代码片段:
private static readonly char[] delimiters =Enumerable.Range(0, 256).Select(i => (char)i).Where(c =>!char.IsLetterOrDigit(c)).ToArray();
private const string textToParse = @"
Call me Ishmael. Some years ago - never mind how long precisely - having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen, and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people's hats off - then, I account it high time to get to sea as soon as I can.
― Herman Melville, Moby Dick.
";
- 在
Main方法中添加以下代码片段:
var q = textToParse.Split(delimiters)
.AsParallel()
.MapReduce(
s => s.ToLower().ToCharArray()
, c => c
, g => new[] {new {Char = g.Key, Count = g.Count()}})
.Where(c => char.IsLetterOrDigit(c.Char))
.OrderByDescending( c => c.Count);
foreach (var info in q)
{
Console.WriteLine("Character {0} occured in the text {1}{2}", info.Char, info.Count, info.Count == 1 ? "time" : "times");
}
Console.WriteLine(" -------------------------------------------");
const string searchPattern = "en";
var q2 = textToParse.Split(delimiters)
.AsParallel()
.Where(s => s.Contains(searchPattern))
.MapReduce(
s => new [] {s}
, s => s
, g => new[] {new {Word = g.Key, Count = g.Count()}})
.OrderByDescending(s => s.Count);
Console.WriteLine("Words with search pattern '{0}':",searchPattern);
foreach (var info in q2)
{
Console.WriteLine("{0} occured in the text {1} {2}",info.Word, info.Count,
info.Count == 1 ? "time" : "times");
}
int halfLengthWordIndex = textToParse.IndexOf(' ',textToParse.Length/2);
using(var sw = File.CreateText("1.txt"))
{
sw.Write(textToParse.Substring(0, halfLengthWordIndex));
}
using(var sw = File.CreateText("2.txt"))
{
sw.Write(textToParse.Substring(halfLengthWordIndex));
}
string[] paths = new[] { ".\\" };
Console.WriteLine(" ------------------------------------------------");
var q3 = paths
.SelectMany(p => Directory.EnumerateFiles(p, "*.txt"))
.AsParallel()
.MapReduce(
path => File.ReadLines(path).SelectMany(line =>line.Trim(delimiters).Split(delimiters)),word => string.IsNullOrWhiteSpace(word) ? '\t' :word.ToLower()[0], g => new [] { new {FirstLetter = g.Key, Count = g.Count()}})
.Where(s => char.IsLetterOrDigit(s.FirstLetter))
.OrderByDescending(s => s.Count);
Console.WriteLine("Words from text files");
foreach (var info in q3)
{
Console.WriteLine("Words starting with letter '{0}'occured in the text {1} {2}", info.FirstLetter,info.Count,
info.Count == 1 ? "time" : "times");
}
- 在
Program类定义之后添加以下代码片段:
static class PLINQExtensions
{
public static ParallelQuery<TResult> MapReduce<TSource,TMapped, TKey, TResult>(
this ParallelQuery<TSource> source,
Func<TSource, IEnumerable<TMapped>> map,
Func<TMapped, TKey> keySelector,
Func<IGrouping<TKey, TMapped>,
IEnumerable<TResult>> reduce)
{
return source.SelectMany(map)
.GroupBy(keySelector)
.SelectMany(reduce);
}
}
- 运行程序。
它是如何工作的...
Map/Reduce函数是另一种重要的并行编程模式。它适用于小型程序和大型多服务器计算。这种模式的含义是你有两个特殊的函数来应用于你的数据。其中一个是Map函数。它以键/值列表形式的一组初始数据,并产生另一个键/值序列,将数据转换为进一步处理的舒适格式。然后我们使用另一个名为Reduce的函数。Reduce函数接受Map函数的结果,并将其转换为我们实际需要的最小可能数据集。要了解这个算法是如何工作的,让我们通过这个示例来看一下。
首先,我们在字符串变量textToParse中定义了一个相对较大的文本。我们需要这个文本来运行我们的查询。然后我们将我们的Map/Reduce实现定义为PLINQExtensions类中的 PLINQ 扩展方法。我们使用SelectMany将初始序列转换为我们需要的序列,通过应用Map函数。这个函数从一个序列元素中产生几个新元素。然后我们选择如何使用keySelector函数对新序列进行分组,并使用GroupBy与这个键来产生一个中间键/值序列。我们做的最后一件事就是对产生的分组序列应用Reduce来得到结果。
在我们的第一个例子中,我们将文本分割成单独的单词,然后我们使用Map函数将每个单词切割成字符序列,并按字符值对结果进行分组。Reduce函数最终将序列转换为键值对,其中我们有一个字符和一个数字,表示它在文本中被使用的次数,按使用次数排序。因此,我们能够并行计算文本中每个字符的出现次数(因为我们使用 PLINQ 来查询初始数据)。
下一个例子非常相似,但现在我们使用 PLINQ 来过滤序列,只留下包含我们搜索模式的单词,然后按它们在文本中的使用情况对所有这些单词进行排序。
最后一个例子使用文件 I/O。我们将示例文本保存在磁盘上,将其分成两个文件。然后我们将Map函数定义为从目录名称生成多个字符串,这些字符串都是初始目录中所有文本文件中所有行中的所有单词。然后我们通过第一个字母对这些单词进行分组(过滤掉空字符串),并使用 reduce 来查看哪个字母在文本中最常用作第一个单词的字母。好处在于我们可以很容易地通过使用其他 map 和 reduce 函数的实现来将此程序分布,并且我们仍然能够使用 PLINQ 来使我们的程序易于阅读和维护。