C-5-多线程秘籍-全-
C#5 多线程秘籍(全)
原文:
zh.annas-archive.org/md5/B7D7E52064DCCDC9755A7421EE8385A4
译者:飞龙
前言
不久以前,典型的个人电脑 CPU 只有一个计算核心,功耗足以在上面煎鸡蛋。2005 年,英特尔推出了其第一款多核 CPU,自那时起,计算机开始朝着不同的方向发展。低功耗和多个计算核心变得比单个计算核心的性能更重要。这也导致了编程范式的变化。现在我们需要学会如何有效地使用所有 CPU 核心以实现最佳性能,同时通过仅在特定时间运行所需的程序来节省电池电量。此外,我们还需要以尽可能高效的方式编写服务器应用程序,以利用多个 CPU 核心甚至多台计算机来支持尽可能多的用户。
要能够创建这样的应用程序,您必须学会有效地在程序中使用多个 CPU 核心。如果您使用 Microsoft .NET 开发平台和 C#编程语言,这本书将是编写性能良好且响应迅速的应用程序的完美起点。
本书的目的是为您提供 C#中多线程和并行编程的逐步指南。我们将从基本概念开始,根据前几章的信息逐渐深入更高级的主题,并以真实世界的并行编程模式和 Windows Store 应用程序示例结束。
本书内容
第一章,“线程基础”,介绍了 C#中线程的基本操作。它解释了线程是什么,使用线程的利弊以及其他重要的线程方面。
第二章,“线程同步”,描述了线程交互的细节。您将了解为什么我们需要协调线程以及组织线程协调的不同方式。
第三章,“使用线程池”,解释了线程池的概念。它展示了如何使用线程池,如何处理异步操作,以及使用线程池的良好和不良实践。
第四章,“使用任务并行库”,深入探讨了任务并行库框架。本章概述了 TPL 的每个重要方面,包括任务组合、异常管理和操作取消。
第五章,“使用 C# 5.0”,详细解释了新的 C# 5.0 特性 - 异步方法。您将了解 async 和 await 关键字的含义,以及如何在不同场景中使用它们,以及 await 在幕后的工作原理。
第六章,“使用并发集合”,描述了.NET Framework 中包含的用于并行算法的标准数据结构。它涵盖了每种数据结构的示例编程场景。
第七章,“使用 PLINQ”,是对并行 LINQ 基础设施的深入探讨。本章描述了任务和数据并行性,对 LINQ 查询进行并行化,调整并行性选项,对查询进行分区,并聚合并行查询结果。
第八章,“响应式扩展”,解释了何时以及如何使用响应式扩展框架。您将学习如何组合事件以及如何针对事件序列执行 LINQ 查询。
第九章,“使用异步 I/O”,详细介绍了包括文件、网络和数据库场景在内的异步 I/O 过程。
第十章,“并行编程模式”,概述了常见的并行编程问题解决方案。
第十一章,更多内容,涵盖了为 Windows 8 编写异步应用程序的方面。您将学习如何使用 Windows 8 异步 API,并在 Windows Store 应用程序中执行后台工作。
本书所需内容
对于大多数的示例,您将需要 Microsoft Visual Studio Express 2012 for Windows Desktop。第十一章的示例将需要 Windows 8 和 Microsoft Visual Studio Express 2012 for Windows 8 来编译 Windows Store 应用程序。
本书适合对象
Multithreading in C# 5.0 Cookbook 是为现有的 C# 开发人员编写的,他们在多线程、异步和并行编程方面几乎没有背景。本书涵盖了从基本概念到使用 C# 和 .NET 生态系统的复杂编程模式和算法的这些主题。
约定
在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些这些样式的示例,以及它们含义的解释。
文本中的代码单词显示如下:“当我们构造一个线程时,ThreadStart
或 ParameterizedThreadStart
委托的实例被传递给构造函数。”
代码块设置如下:
static void PrintNumbers()
{
Console.WriteLine("Starting...");
for (int i = 1; i < 10; i++)
{
Console.WriteLine(i);
}
}
新术语 和 重要单词 以粗体显示。例如,屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中显示为:“启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序 项目。”
注意
警告或重要提示会以这样的方式显示在一个框中。
提示
提示和技巧看起来像这样。
第一章:线程基础知识
在本章中,我们将介绍在 C#中使用线程的基本任务。您将了解到:
-
在 C#中创建线程
-
暂停线程
-
使线程等待
-
中止线程
-
确定线程状态
-
线程优先级
-
前台和后台线程
-
向线程传递参数
-
使用 C#锁定关键字进行锁定
-
使用监视器构造进行锁定
-
处理异常
介绍
在过去的某个时刻,普通计算机只有一个计算单元,无法同时执行多个计算任务。然而,操作系统已经可以同时处理多个程序,实现了多任务的概念。为了防止一个程序永远控制 CPU,导致其他应用程序和操作系统本身挂起,操作系统必须以某种方式将物理计算单元分割成几个虚拟处理器,并为每个执行程序分配一定量的计算能力。此外,操作系统必须始终具有对 CPU 的优先访问权,并且应该能够为不同的程序优先访问 CPU。线程是这一概念的实现。它可以被认为是分配给独立运行的特定程序的虚拟处理器。
注意
请记住,线程会消耗大量的操作系统资源。试图在许多线程之间共享一个物理处理器将导致操作系统忙于管理线程而无法运行程序的情况。
因此,虽然可以增强计算机处理器,使其每秒执行更多命令,但处理线程通常是操作系统的任务。在单核 CPU 上尝试并行计算某些任务是没有意义的,因为这比按顺序运行这些计算需要更多时间。然而,当处理器开始拥有更多的计算核心时,旧程序无法利用这一点,因为它们只使用一个处理器核心。
为了有效地利用现代处理器的计算能力,非常重要的是能够以多线程通信和同步的方式组织程序,从而使用多个计算核心。
本章的示例将重点介绍在 C#语言中使用线程执行一些非常基本的操作。我们将涵盖线程的生命周期,包括创建、挂起、使线程等待和中止线程,然后我们将介绍基本的同步技术。
在 C#中创建线程
在接下来的示例中,我们将使用 Visual Studio 2012 作为编写 C#多线程程序的主要工具。本示例将向您展示如何创建一个新的 C#程序并在其中使用线程。
注意
有免费的 Visual Studio 2012 Express 版本,可以从微软网站下载。我们将需要 Visual Studio 2012 Express for Windows Desktop 来进行大多数示例,以及 Visual Studio 2012 Express for Windows 8 来进行 Windows 8 特定的示例。
准备工作
要完成本示例,您将需要 Visual Studio 2012。没有其他先决条件。本示例的源代码可以在BookSamples\Chapter1\Recipe1
中找到。
提示
下载示例代码
您可以通过您在www.packtpub.com
的帐户下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support
并注册,以便直接通过电子邮件接收文件。
如何做...
要了解如何创建一个新的 C#程序并在其中使用线程,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
确保项目使用.NET Framework 4.0 或更高版本。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void PrintNumbers()
{
Console.WriteLine("Starting...");
for (int i = 1; i < 10; i++)
{
Console.WriteLine(i);
}
}
- 在
Main
方法内部添加以下代码片段:
Thread t = new Thread(PrintNumbers);
t.Start();
PrintNumbers();
- 运行程序。输出将会是这样的:
它是如何工作的...
在步骤 1 和 2 中,我们使用.Net Framework 版本 4.0 创建了一个简单的 C#控制台应用程序。然后在第 3 步中,我们包含了包含程序所需的所有类型的System.Threading
命名空间。
注意
正在执行程序的实例可以称为进程。一个进程由一个或多个线程组成。这意味着当我们运行一个程序时,我们总是有一个执行程序代码的主线程。
在第 4 步中,我们定义了PrintNumbers
方法,该方法将在主线程和新创建的线程中使用。然后在第 5 步中,我们创建了一个运行PrintNumbers
的线程。当我们构造一个线程时,ThreadStart
或ParameterizedThreadStart
委托的实例被传递给构造函数。当我们只需输入要在不同线程中运行的方法的名称时,C#编译器在幕后创建了这个对象。然后我们启动一个线程,并在主线程上以通常的方式运行PrintNumbers
。
结果将会有两个范围从 1 到 10 的数字范围随机交叉。这说明PrintNumbers
方法同时在主线程和另一个线程上运行。
暂停一个线程
这个示例将向您展示如何使一个线程在一段时间内等待,而不浪费操作系统资源。
准备好了
要完成本示例,您需要 Visual Studio 2012。没有其他先决条件。本示例的源代码可以在BookSamples\Chapter1\Recipe2
中找到。
如何做...
要理解如何使一个线程等待而不浪费操作系统资源,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void PrintNumbers()
{
Console.WriteLine("Starting...");
for (int i = 1; i < 10; i++)
{
Console.WriteLine(i);
}
}
static void PrintNumbersWithDelay()
{
Console.WriteLine("Starting...");
for (int i = 1; i < 10; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine(i);
}
}
- 在
Main
方法内部添加以下代码片段:
Thread t = new Thread(PrintNumbersWithDelay);
t.Start();
PrintNumbers();
- 运行程序。
它是如何工作的...
当程序运行时,它创建一个线程,该线程将在PrintNumbersWithDelay
方法中执行代码。在那之后,它立即运行PrintNumbers
方法。这里的关键特点是在PrintNumbersWithDelay
方法中添加Thread.Sleep
方法调用。它会导致执行此代码的线程在打印每个数字之前等待指定的时间(在我们的例子中为两秒)。当一个线程正在睡眠时,它尽可能少地使用 CPU 时间。因此,我们将看到通常稍后运行的PrintNumbers
方法中的代码将在单独的线程中的PrintNumbersWithDelay
方法中的代码之前执行。
使一个线程等待
这个示例将向您展示程序如何等待另一个线程中的某些计算完成,以便稍后在代码中使用其结果。仅使用Thread.Sleep
是不够的,因为我们不知道计算需要多长时间。
准备好了
要完成本示例,您需要 Visual Studio 2012。没有其他先决条件。本示例的源代码可以在BookSamples\Chapter1\Recipe3
中找到。
如何做...
要理解程序如何等待另一个线程中的某些计算完成,以便稍后使用其结果,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void PrintNumbersWithDelay()
{
Console.WriteLine("Starting...");
for (int i = 1; i < 10; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine(i);
}
}
- 在
Main
方法内部添加以下代码片段:
Console.WriteLine("Starting...");
Thread t = new Thread(PrintNumbersWithDelay);
t.Start();
t.Join();
Console.WriteLine("Thread completed");
- 运行程序。
它是如何工作的...
当程序运行时,它运行一个长时间运行的线程,打印出数字,并在打印每个数字之前等待两秒。但在主程序中,我们调用了t.Join
方法,这允许我们等待线程t
完成。当它完成时,主程序继续运行。借助这种技术,可以在两个线程之间同步执行步骤。第一个线程等待另一个完成,然后继续工作。在第一个线程等待时,它处于阻塞状态(就像在之前的示例中调用Thread.Sleep
时一样)。
中止一个线程
在本示例中,我们将描述如何中止另一个线程的执行。
准备工作
要完成本示例,您需要 Visual Studio 2012。没有其他先决条件。本示例的源代码可以在BookSamples\Chapter1\Recipe4
中找到。
如何做...
要了解如何中止另一个线程的执行,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下方添加以下代码片段:
static void PrintNumbersWithDelay()
{
Console.WriteLine("Starting...");
for (int i = 1; i < 10; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine(i);
}
}
- 在
Main
方法内添加以下代码片段:
Console.WriteLine("Starting program...");
Thread t = new Thread(PrintNumbersWithDelay);
t.Start();
Thread.Sleep(TimeSpan.FromSeconds(6));
t.Abort();
Console.WriteLine("A thread has been aborted");
Thread t = new Thread(PrintNumbers);
t.Start();
PrintNumbers();
- 运行程序。
它是如何工作的...
当主程序和一个单独的打印数字的线程运行时,我们等待 6 秒,然后在一个线程上调用t.Abort
方法。这会向线程注入一个ThreadAbortException
方法,导致线程终止。这是非常危险的,通常因为这个异常可能在任何时候发生,可能会完全破坏应用程序。此外,并不总是可能使用这种技术终止线程。目标线程可能拒绝通过处理此异常并调用Thread.ResetAbort
方法来中止。因此,不建议使用Abort
方法来关闭线程。有不同的方法更受推荐,比如提供一个CancellationToken
方法来取消线程执行。这种方法将在第三章使用线程池中描述。
确定线程状态
本示例将描述线程可能具有的可能状态。了解线程是否已启动或是否处于阻塞状态非常有用。请注意,因为线程独立运行,其状态可能随时改变。
准备工作
要完成本示例,您需要 Visual Studio 2012。没有其他先决条件。本示例的源代码可以在BookSamples\Chapter1\Recipe5
中找到。
如何做...
要了解如何确定线程状态并获取有用的信息,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下方添加以下代码片段:
static void DoNothing()
{
Thread.Sleep(TimeSpan.FromSeconds(2));
}
static void PrintNumbersWithStatus()
{
Console.WriteLine("Starting...");
Console.WriteLine(Thread.CurrentThread
.ThreadState.ToString());
for (int i = 1; i < 10; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine(i);
}
}
- 在
Main
方法内添加以下代码片段:
Console.WriteLine("Starting program...");
Thread t = new Thread(PrintNumbersWithStatus);
Thread t2 = new Thread(DoNothing);
Console.WriteLine(t.ThreadState.ToString());
t2.Start();
t.Start();
for (int i = 1; i < 30; i++)
{
Console.WriteLine(t.ThreadState.ToString());
}
Thread.Sleep(TimeSpan.FromSeconds(6));
t.Abort();
Console.WriteLine("A thread has been aborted");
Console.WriteLine(t.ThreadState.ToString());
Console.WriteLine(t2.ThreadState.ToString());
- 运行程序。
它是如何工作的...
当主程序启动时,它定义了两个不同的线程;其中一个将被中止,另一个成功运行。线程状态位于Thread
对象的ThreadState
属性中,这是一个 C#枚举。一开始,线程处于ThreadState.Unstarted
状态。然后我们运行它,并假设在 30 次循环迭代的过程中,线程将从ThreadState.Running
状态变为ThreadState.WaitSleepJoin
状态。
提示
请注意,当前的Thread
对象始终可以通过Thread.CurrentThread
静态属性访问。
如果没有发生,只需增加迭代次数。然后我们中止第一个线程,并看到现在它有一个ThreadState.Aborted
状态。程序也可能打印出ThreadState.AbortRequested
状态。这很好地说明了同步两个线程的复杂性。请记住,您不应该在程序中使用线程中止。我在这里只是为了展示相应的线程状态。
最后,我们可以看到我们的第二个线程t2
成功完成,现在有一个ThreadState.Stopped
状态。还有其他几种状态,但它们部分已被弃用,部分不如我们检查的那些有用。
线程优先级
本示例将描述线程优先级的不同可能选项。设置线程优先级确定线程将获得多少 CPU 时间。
准备就绪
要完成这个示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter1\Recipe6
中找到。
如何做...
要理解线程优先级的工作原理,请执行以下步骤:
-
开始 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Diagnostics;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void RunThreads()
{
var sample = new ThreadSample();
var threadOne = new Thread(sample.CountNumbers);
threadOne.Name = "ThreadOne";
var threadTwo = new Thread(sample.CountNumbers);
threadTwo.Name = "ThreadTwo";
threadOne.Priority = ThreadPriority.Highest;
threadTwo.Priority = ThreadPriority.Lowest;
threadOne.Start();
threadTwo.Start();
Thread.Sleep(TimeSpan.FromSeconds(2));
sample.Stop();
}
class ThreadSample
{
private bool _isStopped = false;
public void Stop()
{
_isStopped = true;
}
public void CountNumbers()
{
long counter = 0;
while (!_isStopped)
{
counter++;
}
Console.WriteLine("{0} with {1,11} priority " +"has a count = {2,13}", Thread.CurrentThread.Name, Thread.CurrentThread.Priority,counter.ToString("N0"));
}
}
- 在
Main
方法内添加以下代码片段:
Console.WriteLine("Current thread priority: {0}", Thread.CurrentThread.Priority);
Console.WriteLine("Running on all cores available");
RunThreads();
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("Running on a single core");
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
RunThreads();
- 运行程序。
它是如何工作的...
当主程序启动时,它定义了两个不同的线程。第一个是ThreadPriority.Highest
,将具有最高的线程优先级,而第二个是ThreadPriority.Lowest
,将具有最低的优先级。我们打印出主线程优先级值,然后在所有可用的核心上启动这两个线程。如果我们有多个计算核心,我们应该在两秒内得到一个初始结果。最高优先级线程通常应该计算更多迭代,但两个值应该接近。但是,如果有其他程序运行并加载所有 CPU 核心,情况可能会大不相同。
为了模拟这种情况,我们设置了ProcessorAffinity
选项,指示操作系统在单个 CPU 核心(编号为一)上运行所有线程。现在结果应该非常不同,计算将花费超过 2 秒。这是因为 CPU 核心将主要运行高优先级线程,给其他线程很少的时间。
请注意,这是操作系统如何处理线程优先级的示例。通常,您不应该编写依赖于此行为的程序。
前台和后台线程
本示例将描述前台和后台线程是什么,以及设置此选项如何影响程序行为。
准备就绪
要完成这个示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter1\Recipe7
中找到。
如何做...
要理解前台和后台线程对程序的影响,请执行以下操作:
-
开始 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
class ThreadSample
{
private readonly int _iterations;
public ThreadSample(int iterations)
{
_iterations = iterations;
}
public void CountNumbers()
{
for (int i = 0; i < _iterations; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(0.5));
Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);
}
}
}
- 在
Main
方法内添加以下代码片段:
var sampleForeground = new ThreadSample(10);
var sampleBackground = new ThreadSample(20);
var threadOne = new Thread(sampleForeground.CountNumbers);
threadOne.Name = "ForegroundThread";
var threadTwo = new Thread(sampleBackground.CountNumbers);
threadTwo.Name = "BackgroundThread";
threadTwo.IsBackground = true;
threadOne.Start();
threadTwo.Start();
- 运行程序。
它是如何工作的...
当主程序启动时,它定义了两个不同的线程。默认情况下,我们显式创建的线程是前台线程。要创建后台线程,我们手动将threadTwo
对象的IsBackground
属性设置为true
。我们以第一个线程将更快完成的方式配置这些线程,然后运行程序。
第一个线程完成后,程序关闭,后台线程终止。这是两者之间的主要区别:进程在完成工作之前等待所有前台线程完成,但如果有后台线程,它们只是关闭。
还要注意的是,如果程序定义了一个前台线程,而这个线程没有完成,主程序将无法正常结束。
向线程传递参数
这个示例将描述如何向在另一个线程中运行的代码提供所需的数据。我们将介绍不同的方式来完成这个任务,并审查常见的错误。
准备工作
要完成这个示例,您需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter1\Recipe8
中找到。
如何做...
要了解如何向线程传递参数,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void Count(object iterations)
{
CountNumbers((int)iterations);
}
static void CountNumbers(int iterations)
{
for (int i = 1; i <= iterations; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(0.5));
Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);
}
}
static void PrintNumber(int number)
{
Console.WriteLine(number);
}
class ThreadSample
{
private readonly int _iterations;
public ThreadSample(int iterations)
{
_iterations = iterations;
}
public void CountNumbers()
{
for (int i = 1; i <= _iterations; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(0.5));
Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);
}
}
}
- 在
Main
方法内部添加以下代码片段:
var sample = new ThreadSample(10);
var threadOne = new Thread(sample.CountNumbers);
threadOne.Name = "ThreadOne";
threadOne.Start();
threadOne.Join();
Console.WriteLine("--------------------------");
var threadTwo = new Thread(Count);
threadTwo.Name = "ThreadTwo";
threadTwo.Start(8);
threadTwo.Join();
Console.WriteLine("--------------------------");
var threadThree = new Thread(() => CountNumbers(12));
threadThree.Name = "ThreadThree";
threadThree.Start();
threadThree.Join();
Console.WriteLine("--------------------------");
int i = 10;
var threadFour = new Thread(() => PrintNumber(i));
i = 20;
var threadFive = new Thread(() => PrintNumber(i));
threadFour.Start();
threadFive.Start();
- 运行程序。
工作原理...
当主程序启动时,首先创建一个ThreadSample
类的对象,并为其提供一定数量的迭代次数。然后我们使用对象的方法CountNumbers
启动一个线程。这个方法在另一个线程中运行,但它使用数字 10,这是我们传递给对象构造函数的值。因此,我们只是以同样间接的方式将这个迭代次数传递给另一个线程。
还有更多...
另一种传递数据的方式是使用Thread.Start
方法,接受一个可以传递给另一个线程的对象。为了以这种方式工作,我们在另一个线程中启动的方法必须接受一个类型为 object 的单个参数。通过创建一个threadTwo
线程来说明这个选项。我们将8
作为一个对象传递给Count
方法,在那里它被转换为integer
类型。
下一个选项涉及使用 lambda 表达式。lambda 表达式定义了一个不属于任何类的方法。我们创建这样一个方法,调用另一个方法所需的参数,并在另一个线程中启动它。当我们启动threadThree
线程时,它打印出 12 个数字,这些数字正是我们通过 lambda 表达式传递给它的数字。
使用 lambda 表达式涉及另一个名为闭包
的 C#构造。当我们在 lambda 表达式中使用任何局部变量时,C#会生成一个类,并将这个变量作为这个类的属性。因此,实际上,我们做的事情与threadOne
线程中的一样,但我们不是自己定义这个类;C#编译器会自动完成这个工作。
这可能会导致几个问题;例如,如果我们从几个 lambda 中使用相同的变量,它们实际上会共享这个变量的值。这可以通过前面的例子来说明;当我们启动threadFour
和threadFive
时,它们都会打印出20
,因为在启动这两个线程之前,变量已经被更改为持有值20
。
使用 C#锁定关键字进行锁定
这个示例将描述如何确保一个线程使用某个资源时,另一个线程不会同时使用它。我们将看到为什么需要这样做,以及线程安全概念是什么。
准备工作
要完成这个示例,您需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter1\Recipe9
中找到。
如何做...
要了解如何使用 C#锁定关键字,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void TestCounter(CounterBase c)
{
for (int i = 0; i < 100000; i++)
{
c.Increment();
c.Decrement();
}
}
class Counter : CounterBase
{
public int Count { get; private set; }
public override void Increment()
{
Count++;
}
public override void Decrement()
{
Count--;
}
}
class CounterWithLock : CounterBase
{
private readonly object _syncRoot = new Object();
public int Count { get; private set; }
public override void Increment()
{
lock (_syncRoot)
{
Count++;
}
}
public override void Decrement()
{
lock (_syncRoot)
{
Count--;
}
}
}
abstract class CounterBase
{
public abstract void Increment();
public abstract void Decrement();
}
- 在
Main
方法内部添加以下代码片段:
Console.WriteLine("Incorrect counter");
var c = new Counter();
var t1 = new Thread(() => TestCounter(c));
var t2 = new Thread(() => TestCounter(c));
var t3 = new Thread(() => TestCounter(c));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine("Total count: {0}",c.Count);
Console.WriteLine("--------------------------");
Console.WriteLine("Correct counter");
var c1 = new CounterWithLock();
t1 = new Thread(() => TestCounter(c1));
t2 = new Thread(() => TestCounter(c1));
t3 = new Thread(() => TestCounter(c1));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine("Total count: {0}", c1.Count);
- 运行程序。
工作原理...
当主程序启动时,首先创建一个Counter
类的对象。这个类定义了一个简单的计数器,可以进行增加和减少。然后我们启动三个线程,它们共享同一个计数器实例,并在一个循环中执行增加和减少操作。这会导致不确定的结果。如果我们多次运行程序,会打印出几个不同的计数器值。它可能是零,但大多数情况下不会是。
这是因为Counter
类不是线程安全的。当多个线程同时访问计数器时,第一个线程获取计数器值为10
并将其增加到 11。然后第二个线程获取值 11 并将其增加到 12。第一个线程获取计数器值 12,但在减少之前,第二个线程也获取了计数器值 12。然后第一个线程将 12 减少到 11 并保存到计数器中,而第二个线程同时也做同样的操作。结果是我们有两次增加和只有一次减少,这显然是不对的。这种情况被称为竞争条件,是多线程环境中错误的一个常见原因。
为了确保这种情况不会发生,我们必须确保当一个线程使用计数器时,所有其他线程必须等待,直到第一个线程完成工作。我们可以使用lock
关键字来实现这种行为。如果我们lock
一个对象,所有需要访问这个对象的其他线程将会处于阻塞状态,直到它被解锁。这可能会导致严重的性能问题,稍后在第二章中,线程同步,我们将学到更多关于这个的知识。
使用 Monitor 构造锁定
这个示例说明了另一个常见的多线程错误,称为死锁。由于死锁会导致程序停止工作,所以这个示例的第一部分是一个新的Monitor
构造,它允许我们避免死锁。然后,之前描述的lock
关键字被用来产生死锁。
准备就绪
要完成这个示例,你需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter1\Recipe10
中找到。
如何做...
要理解多线程错误死锁,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void LockTooMuch(object lock1, object lock2)
{
lock (lock1)
{
Thread.Sleep(1000);
lock (lock2);
}
}
- 在
Main
方法中添加以下代码片段:
object lock1 = new object();
object lock2 = new object();
new Thread(() => LockTooMuch(lock1, lock2)).Start();
lock (lock2)
{
Thread.Sleep(1000);
Console.WriteLine("Monitor.TryEnter allows not to get stuck, returning false after a specified timeout is elapsed");
if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5)))
{
Console.WriteLine("Acquired a protected resource succesfully");
}
else
{
Console.WriteLine("Timeout acquiring a resource!");
}
}
new Thread(() => LockTooMuch(lock1, lock2)).Start();
Console.WriteLine("----------------------------------");
lock (lock2)
{
Console.WriteLine("This will be a deadlock!");
Thread.Sleep(1000);
lock (lock1)
{
Console.WriteLine("Acquired a protected resource succesfully");
}
}
- 运行程序。
它是如何工作的...
让我们从LockTooMuch
方法开始。在这个方法中,我们只是锁定第一个对象,等待一秒,然后锁定第二个对象。然后我们在另一个线程中启动这个方法,并尝试从主线程锁定第二个对象,然后锁定第一个对象。
如果我们像示例的第二部分那样使用lock
关键字,就会发生死锁。第一个线程持有lock1
对象的lock
并等待lock2
对象释放;主线程持有lock2
对象的lock
并等待lock1
对象释放,而在这种情况下永远不会发生。
实际上,lock
关键字是对Monitor
类使用的一种语法糖。如果我们反汇编带有lock
的代码,我们会看到它转换成以下代码片段:
bool acquiredLock = false;
try
{
Monitor.Enter(lockObject, ref acquiredLock);
// Code that accesses resources that are protected by the lock.
}
finally
{
if (acquiredLock)
{
Monitor.Exit(lockObject);
}
}
因此,我们可以直接使用Monitor
类;它有TryEnter
方法,接受一个超时参数,并在我们无法获取由lock
保护的资源之前超时返回false
。
处理异常
这个示例将描述如何正确处理其他线程中的异常。在线程内部始终放置一个try/catch
块非常重要,因为在线程代码外部无法捕获异常。
准备就绪
要完成这个示例,你需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter1\Recipe11
中找到。
如何做到…
要理解其他线程中异常的处理,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void BadFaultyThread()
{
Console.WriteLine("Starting a faulty thread...");
Thread.Sleep(TimeSpan.FromSeconds(2));
throw new Exception("Boom!");
}
static void FaultyThread()
{
try
{
Console.WriteLine("Starting a faulty thread...");
Thread.Sleep(TimeSpan.FromSeconds(1));
throw new Exception("Boom!");
}
catch (Exception ex)
{
Console.WriteLine("Exception handled: {0}", ex.Message);
}
}
- 在
Main
方法内部添加以下代码片段:
var t = new Thread(FaultyThread);
t.Start();
t.Join();
try
{
t = new Thread(BadFaultyThread);
t.Start();
}
catch (Exception ex)
{
Console.WriteLine("We won't get here!");
}
- 运行程序。
它是如何工作的…
当主程序启动时,它定义了两个将抛出异常的线程。其中一个线程处理异常,而另一个不处理。你可以看到第二个异常没有被try/catch
块捕获,而是在启动线程的代码周围。因此,如果直接使用线程,一般规则是不要从线程中抛出异常,而是在线程代码内部使用try/catch
块。
在较旧版本的.NET Framework(1.0 和 1.1)中,这种行为是不同的,未捕获的异常不会强制应用程序关闭。可以通过添加一个应用程序配置文件(如app.config
)来使用此策略,其中包含以下代码片段:
<configuration>
<runtime>
<legacyUnhandledExceptionPolicy enabled="1" />
</runtime>
</configuration>
第二章:线程同步
在本章中,我们将描述一些处理多个线程共享资源的常见技术。您将了解:
-
执行基本的原子操作
-
使用 Mutex 构造
-
使用 SemaphoreSlim 构造
-
使用 AutoResetEvent 构造
-
使用 ManualResetEventSlim 构造
-
使用 CountDownEvent 构造
-
使用 Barrier 构造
-
使用 ReaderWriterLockSlim 构造
-
使用 SpinWait 构造
介绍
正如我们在第一章 线程基础中看到的那样,同时从多个线程使用共享对象是有问题的。非常重要的是同步这些线程,以便它们按适当的顺序对共享对象执行操作。在多线程计数器示例中,我们遇到了一个称为竞争条件的问题。这是因为多个线程的执行没有得到适当的同步。当一个线程执行增量和减量操作时,其他线程必须等待它们的轮到。这个一般问题通常被称为线程同步。
有几种方法可以实现线程同步。首先,如果没有共享对象,就根本不需要同步。令人惊讶的是,我们经常可以通过重新设计程序并消除共享状态来摆脱复杂的同步构造。如果可能的话,尽量避免多个线程使用单个对象。
如果我们必须有共享状态,第二种方法是只使用原子操作。这意味着一个操作需要一个时间量并立即完成,因此在第一个操作完成之前,没有其他线程可以执行另一个操作。因此,没有必要让其他线程等待此操作完成,也没有必要使用锁;这反过来排除了死锁的情况。
如果这不可能,程序逻辑更复杂,那么我们必须使用不同的构造来协调线程。其中一组构造将等待线程置于阻塞状态。在阻塞状态下,线程使用尽可能少的 CPU 时间。然而,这意味着它将至少包括一个所谓的上下文切换 - 操作系统的线程调度程序将保存等待线程的状态,并切换到另一个线程,轮流恢复其状态。这需要大量资源;但是,如果线程将被暂停很长时间,这是好的。这些构造也被称为内核模式构造,因为只有操作系统的内核能够阻止线程使用 CPU 时间。
如果我们必须等待很短的时间,最好是简单地等待而不是将线程切换到阻塞状态。这将节省我们上下文切换的开销,但会浪费一些 CPU 时间,因为线程在等待时会浪费一些 CPU 时间。这些构造被称为用户模式构造。它们非常轻量级和快速,但在线程必须长时间等待时会浪费大量 CPU 时间。
为了兼顾两者的优点,有混合构造;这些构造首先尝试使用用户模式等待,然后如果线程等待足够长的时间,它将切换到阻塞状态,节省 CPU 资源。
在本章中,我们将研究线程同步的各个方面。我们将介绍如何执行原子操作以及如何使用.NET 框架中包含的现有同步构造。
执行基本的原子操作
这个示例将向您展示如何对对象执行基本的原子操作,以防止竞争条件而不阻塞线程。
准备工作
要通过本示例,您需要 Visual Studio 2012. 没有其他先决条件。此示例的源代码可以在7644_Code\Chapter2\Recipe1
中找到。
如何做...
为了理解基本的原子操作,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下方,添加以下代码片段:
static void TestCounter(CounterBase c)
{
for (int i = 0; i < 100000; i++)
{
c.Increment();
c.Decrement();
}
}
class Counter : CounterBase
{
private int _count;
public int Count { get { return _count; } }
public override void Increment()
{
_count++;
}
public override void Decrement()
{
_count--;
}
}
class CounterNoLock : CounterBase
{
private int _count;
public int Count { get { return _count; } }
public override void Increment()
{
Interlocked.Increment(ref _count);
}
public override void Decrement()
{
Interlocked.Decrement(ref _count);
}
}
abstract class CounterBase
{
public abstract void Increment();
public abstract void Decrement();
}
- 在
Main
方法中,添加以下代码片段:
Console.WriteLine("Incorrect counter");
var c = new Counter();
var t1 = new Thread(() => TestCounter(c));
var t2 = new Thread(() => TestCounter(c));
var t3 = new Thread(() => TestCounter(c));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine("Total count: {0}", c.Count);
Console.WriteLine("--------------------------");
Console.WriteLine("Correct counter");
var c1 = new CounterNoLock();
t1 = new Thread(() => TestCounter(c1));
t2 = new Thread(() => TestCounter(c1));
t3 = new Thread(() => TestCounter(c1));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine("Total count: {0}", c1.Count);
- 运行程序。
工作原理...
当程序运行时,它会创建三个线程,这些线程将执行TestCounter
方法中的代码。该方法在对象上运行一系列的增量/减量操作。最初,Counter
对象是不安全的,我们在这里遇到了竞争条件。因此,在第一种情况下,计数器值是不确定的。我们可能会得到一个零值;然而,如果你多次运行程序,最终会得到一些不正确的非零结果。
在第一章中,线程基础,我们通过锁定对象来解决了这个问题,导致其他线程在一个线程获取旧计数器值时被阻塞,然后计算并将新值分配给计数器。然而,如果我们以这种方式执行这个操作,它是无法在中途停止的;我们可以在没有任何锁定的情况下使用Interlocked
构造来实现正确的结果。它提供了原子方法Increment
、Decrement
和Add
用于基本数学运算,并帮助我们编写Counter
类而不使用锁定。
使用 Mutex 构造
这个示例将描述如何使用Mutex
构造同步两个独立的程序。Mutex
是一种原始同步,它只允许一个线程独占共享资源。
准备工作
要执行这个示例,你需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在7644_Code\Chapter2\Recipe2
中找到。
如何操作...
要理解如何使用Mutex
构造同步两个独立的程序,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法中,添加以下代码片段:
const string MutexName = "CSharpThreadingCookbook";
using (var m = new Mutex(false, MutexName))
{
if (!m.WaitOne(TimeSpan.FromSeconds(5), false))
{
Console.WriteLine("Second instance is running!");
}
else
{
Console.WriteLine("Running!");
Console.ReadLine();
m.ReleaseMutex();
}
}
- 运行程序。
工作原理...
当主程序启动时,它使用特定名称定义了一个互斥体,并将initialOwner
标志设置为false
。这允许程序在互斥体已经创建时获取互斥体。然后,如果没有获取到互斥体,程序将简单地显示Running,并等待按下任意键来释放互斥体并退出。
如果我们启动程序的第二个副本,它将等待 5 秒,尝试获取互斥体。如果我们在程序的第一个副本中按下任意键,第二个副本将开始执行。然而,如果我们继续等待 5 秒,程序的第二个副本将无法获取互斥体。
提示
请注意,命名的互斥体是一个全局操作系统对象!始终正确关闭互斥体;最好的选择是使用块来包装互斥体对象。
这使得在不同程序中同步线程成为可能,这在许多场景下都是有用的。
使用 SemaphoreSlim 构造
这个示例将展示如何SemaphoreSlim
是Semaphore
的轻量级版本;它限制了可以同时访问资源的线程数量。
准备工作
要执行这个示例,你需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter2\Recipe3
中找到。
如何操作...
要理解如何使用SemaphoreSlim
构造限制对资源的多线程访问,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下方,添加以下代码片段:
static SemaphoreSlim _semaphore = new SemaphoreSlim(4);
static void AccessDatabase(string name, int seconds)
{
Console.WriteLine("{0} waits to access a database", name);
_semaphore.Wait();
Console.WriteLine("{0} was granted an access to a database",name);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("{0} is completed", name);
_semaphore.Release();
}
- 在
Main
方法中,添加以下代码片段:
for (int i = 1; i <= 6; i++)
{
string threadName = "Thread " + i;
int secondsToWait = 2 + 2*i;
var t = new Thread(() => AccessDatabase(threadName, secondsToWait));
t.Start();
}
- 运行程序。
工作原理...
当主程序启动时,它创建了一个SemaphoreSlim
实例,在其构造函数中指定了允许的并发线程数。然后启动六个具有不同名称和启动时间的线程来运行。
每个线程都试图访问数据库,但我们通过信号量限制了对数据库的并发访问数量为四个线程。当四个线程访问数据库时,其他两个线程将等待,直到先前的一个线程完成其工作并通过调用_semaphore.Release
方法发出信号。
还有更多...
在这里,我们使用了一个混合构造,它允许我们在等待时间较短的情况下节省上下文切换。然而,这个构造的旧版本称为Semaphore
。这个版本是一个纯的内核时间构造。除了一个非常重要的场景之外,没有使用它的意义;我们可以创建一个命名信号量,就像创建一个命名互斥体一样,并且用它来同步不同程序中的线程。SemaphoreSlim
不使用 Windows 内核信号量,也不支持进程间同步,因此在这种情况下使用Semaphore
。
使用 AutoResetEvent 构造
在本教程中,有一个示例,说明如何使用AutoResetEvent
构造从一个线程向另一个线程发送通知。AutoResetEvent
通知等待的线程事件已发生。
准备工作
要按照本教程进行操作,您需要 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在7644_Code\Chapter2\Recipe4
中找到。
如何做...
要理解如何使用AutoResetEvent
构造从一个线程向另一个线程发送通知,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面,添加以下代码片段:
private static AutoResetEvent _workerEvent = newAutoResetEvent(false);
private static AutoResetEvent _mainEvent = newAutoResetEvent(false);
static void Process(int seconds)
{
Console.WriteLine("Starting a long running work...");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("Work is done!");
_workerEvent.Set();
Console.WriteLine("Waiting for a main thread to completeits work");
_mainEvent.WaitOne();
Console.WriteLine("Starting second operation...");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("Work is done!");
_workerEvent.Set();
}
- 在
Main
方法内部,添加以下代码片段:
var t = new Thread(() => Process(10));
t.Start();
Console.WriteLine("Waiting for another thread to completework");
_workerEvent.WaitOne();
Console.WriteLine("First operation is completed!");
Console.WriteLine("Performing an operation on a mainthread");
Thread.Sleep(TimeSpan.FromSeconds(5));
_mainEvent.Set();
Console.WriteLine("Now running the second operation on asecond thread");
_workerEvent.WaitOne();
Console.WriteLine("Second operation is completed!");
- 运行程序。
它是如何工作的...
当主程序启动时,它定义了两个AutoResetEvent
实例。其中一个是从第二个线程向主线程发出信号的,另一个是从主线程向第二个线程发出信号的。我们在AutoResetEvent
构造函数中提供false
,指定了这两个实例的初始状态为未发出信号
。这意味着调用这些对象中的一个的WaitOne
方法的任何线程都将被阻塞,直到我们调用Set
方法。如果我们将事件状态初始化为true
,它将变为发出信号
,然后第三个调用WaitOne
的线程将立即继续。然后事件状态会自动变为未发出信号
,因此我们需要再次调用Set
方法,以便让其他线程调用这个实例上的WaitOne
方法继续。
然后我们创建一个第二个线程,它将执行第一个操作 10 秒,并等待来自第二个线程的信号。信号意味着第一个操作已完成。现在第二个线程正在等待来自主线程的信号。我们在主线程上做一些额外的工作,并通过调用_mainEvent.Set
方法发送一个信号。然后我们等待来自第二个线程的另一个信号。
AutoResetEvent
是一个内核时间构造,因此如果等待时间不重要,最好使用下一个使用ManualResetEventslim
的教程,这是一个混合构造。
使用 ManualResetEventSlim 构造
本教程将描述如何使用ManualResetEventSlim
构造使线程之间的信号更加灵活。
准备工作
要按照本教程进行操作,您需要 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在BookSamples\Chapter2\Recipe5
中找到。
如何做...
要理解ManualResetEventSlim
构造的使用,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下方,添加以下代码:
static ManualResetEventSlim _mainEvent = new ManualResetEventSlim(false);
static void TravelThroughGates(string threadName,int seconds)
{
Console.WriteLine("{0} falls to sleep", threadName);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("{0} waits for the gates to open!",threadName);
_mainEvent.Wait();
Console.WriteLine("{0} enters the gates!", threadName);
}
- 在
Main
方法中,添加以下代码:
var t1 = new Thread(() => TravelThroughGates("Thread 1",5));
var t2 = new Thread(() => TravelThroughGates("Thread 2",6));
var t3 = new Thread(() => TravelThroughGates("Thread 3",12));
t1.Start();
t2.Start();
t3.Start();
Thread.Sleep(TimeSpan.FromSeconds(6));
Console.WriteLine("The gates are now open!");
_mainEvent.Set();
Thread.Sleep(TimeSpan.FromSeconds(2));
_mainEvent.Reset();
Console.WriteLine("The gates have been closed!");
Thread.Sleep(TimeSpan.FromSeconds(10));
Console.WriteLine("The gates are now open for the secondtime!");
_mainEvent.Set();
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("The gates have been closed!");
_mainEvent.Reset();
- 运行程序。
工作原理...
当主程序启动时,首先创建ManualResetEventSlim
构造的实例。然后我们启动三个线程,它们将等待此事件发出信号以继续执行。
使用此构造的整个过程就像让人们通过一个大门。我们在上一个示例中看到的AutoResetEvent
事件就像一个旋转门,一次只允许一个人通过。ManualResetEventSlim
是ManualResetEvent
的混合版本,直到我们手动调用Reset
方法之前都保持打开。回到代码,当我们调用_mainEvent.Set
时,我们打开它并允许准备接受此信号并继续工作的线程。然而,第三个线程仍在休眠,来不及。我们调用_mainEvent.Reset
,因此关闭它。最后一个线程现在准备好继续,但必须等待下一个信号,这将在几秒钟后发生。
还有更多...
与之前的某个示例一样,我们使用了一个混合构造,它缺乏在操作系统级别工作的可能性。如果我们需要全局事件,我们应该使用EventWaitHandle
构造,它是AutoResetEvent
和ManualResetEvent
的基类。
使用 CountDownEvent 构造
本示例将描述如何使用CountdownEvent
信号构造等待直到某个操作完成。
准备工作
要执行此示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter2\Recipe6
中找到。
如何做...
要理解CountDownEvent
构造的使用,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下方,添加以下代码:
static CountdownEvent _countdown = new CountdownEvent(2);
static void PerformOperation(string message, int seconds)
{
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine(message);
_countdown.Signal();
}
- 在
Main
方法中,添加以下代码:
Console.WriteLine("Starting two operations");
var t1 = new Thread(() => PerformOperation("Operation 1 iscompleted", 4));
var t2 = new Thread(() => PerformOperation("Operation 2 iscompleted", 8));
t1.Start();
t2.Start();
_countdown.Wait();
Console.WriteLine("Both operations have been completed.");
_countdown.Dispose();
- 运行程序。
工作原理...
当主程序启动时,我们创建一个新的CountdownEvent
实例,在其构造函数中指定我们希望在两个操作完成时发出信号。然后我们启动两个线程,在完成时向事件发出信号。一旦第二个线程完成,主线程就会从等待CountdownEvent
中返回并继续进行。使用此构造,非常方便等待多个异步操作完成。
然而,有一个重大缺点;如果我们未能调用所需次数的_countdown.Signal()
,_countdown.Wait()
将永远等待。在使用CountdownEvent
时,请确保所有线程都使用Signal
方法调用完成。
使用屏障构造
这个示例说明了另一个有趣的同步构造,称为Barrier
。Barrier
构造有助于组织多个线程在某个时间点相遇,并提供一个回调,每当线程调用SignalAndWait
方法时都会执行。
准备工作
要执行此示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter2\Recipe7
中找到。
如何做...
要理解Barrier
构造的使用,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下方,添加以下代码:
static Barrier _barrier = new Barrier(2,
b => Console.WriteLine("End of phase {0}",b.CurrentPhaseNumber + 1));
static void PlayMusic(string name, string message,int seconds)
{
for (int i = 1; i < 3; i++)
{
Console.WriteLine("----------------------------------------------");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("{0} starts to {1}", name, message);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine("{0} finishes to {1}", name,message);
_barrier.SignalAndWait();
}
}
- 在
Main
方法中,添加以下代码:
var t1 = new Thread(() => PlayMusic("the guitarist","play an amazing solo", 5));
var t2 = new Thread(() => PlayMusic("the singer","sing his song", 2));
t1.Start();
t2.Start();
- 运行程序。
工作原理...
我们创建了一个Barrier
构造,指定我们要同步两个线程,并且在这两个线程中的每个调用了_barrier.SignalAndWait
方法后,我们需要执行一个回调,打印出完成的阶段数。
每个线程将向Barrier
发送信号两次,因此我们将有两个阶段。每当两个线程都调用SignalAndWait
方法时,Barrier
将执行回调。这对于使用多线程迭代算法进行工作很有用,以在每次迭代结束时执行一些计算。当最后一个线程调用SignalAndWait
方法时,迭代结束。
使用“ReaderWriterLockSlim”构造
本教程将描述如何使用ReaderWriterLockSlim
构造创建一个线程安全的机制,以从多个线程读取和写入集合。ReaderWriterLockSlim
表示用于管理对资源的访问的锁,允许多个线程进行读取或独占访问进行写入。
准备工作
要执行本教程,您将需要 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在BookSamples\Chapter2\Recipe8
中找到。
如何做...
了解如何创建一个线程安全的机制,以使用ReaderWriterLockSlim
构造从多个线程读取和写入集合。
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Collections.Generic;
using System.Threading;
- 在
Main
方法下面,添加以下代码:
static ReaderWriterLockSlim _rw = newReaderWriterLockSlim();
static Dictionary<int, int> _items =new Dictionary<int, int>();
static void Read()
{
Console.WriteLine("Reading contents of a dictionary");
while (true)
{
try
{
_rw.EnterReadLock();
foreach (var key in _items.Keys)
{
Thread.Sleep(TimeSpan.FromSeconds(0.1));
}
}
finally
{
_rw.ExitReadLock();
}
}
}
static void Write(string threadName)
{
while (true)
{
try
{
int newKey = new Random().Next(250);
_rw.EnterUpgradeableReadLock();
if (!_items.ContainsKey(newKey))
{
try
{
_rw.EnterWriteLock();
_items[newKey] = 1;
Console.WriteLine("New key {0} is added to adictionary by a {1}", newKey, threadName);
}
finally
{
_rw.ExitWriteLock();
}
}
Thread.Sleep(TimeSpan.FromSeconds(0.1));
}
finally
{
_rw.ExitUpgradeableReadLock();
}
}
}
- 在
Main
方法中,添加以下代码:
new Thread(Read){ IsBackground = true }.Start();
new Thread(Read){ IsBackground = true }.Start();
new Thread(Read){ IsBackground = true }.Start();
new Thread(() => Write("Thread 1")){ IsBackground =true }.Start();
new Thread(() => Write("Thread 2")){ IsBackground =true }.Start();
Thread.Sleep(TimeSpan.FromSeconds(30));
- 运行程序。
工作原理...
当主程序启动时,它同时运行三个从字典中读取数据的线程和两个向该字典中写入一些数据的线程。为了实现线程安全,我们使用了专为这种情况设计的ReaderWriterLockSlim
构造。
它有两种锁:读取锁允许多个线程读取,写入锁阻止其他线程的每个操作,直到释放此写入锁。还有一个有趣的场景,当我们获取读取锁,从集合中读取一些数据,并根据该数据决定获取写入锁并更改集合。如果我们立即获得写锁,会花费太多时间,不允许我们的读取器读取数据,因为当我们获取写锁时,集合被阻塞。为了最小化这段时间,有EnterUpgradeableReadLock
/ExitUpgradeableReadLock
方法。我们获取读取锁并读取数据;如果我们发现我们必须更改底层集合,我们只需使用EnterWriteLock
方法升级我们的锁,然后快速执行写操作,并使用ExitWriteLock
释放写锁。
在我们的情况下,我们得到一个随机数;然后我们获取一个读取锁,并检查该数字是否存在于字典键集合中。如果不存在,我们将升级我们的锁为写锁,然后将这个新键添加到字典中。最好使用try
/finally
块来确保我们在获取锁后始终释放锁。
我们的所有线程都已创建为后台线程,并在等待 30 秒后,主线程以及所有后台线程都完成。
使用 SpinWait 构造
本教程将描述如何在不涉及内核模式构造的情况下等待线程。此外,我们介绍了SpinWait
,这是一种混合同步构造,旨在在用户模式下等待一段时间,然后切换到内核模式以节省 CPU 时间。
准备工作
要执行本教程,您将需要 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在BookSamples\Chapter2\Recipe9
中找到。
如何做...
了解在不涉及内核模式构造的情况下等待线程,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面,添加以下代码:
static volatile bool _isCompleted = false;
static void UserModeWait()
{
while (!_isCompleted)
{
Console.Write(".");
}
Console.WriteLine();
Console.WriteLine("Waiting is complete");
}
static void HybridSpinWait()
{
var w = new SpinWait();
while (!_isCompleted)
{
w.SpinOnce();
Console.WriteLine(w.NextSpinWillYield);
}
Console.WriteLine("Waiting is complete");
}
- 在
Main
方法中,添加以下代码:
var t1 = new Thread(UserModeWait);
var t2 = new Thread(HybridSpinWait);
Console.WriteLine("Running user mode waiting");
t1.Start();
Thread.Sleep(20);
_isCompleted = true;
Thread.Sleep(TimeSpan.FromSeconds(1));
_isCompleted = false;
Console.WriteLine("Running hybrid SpinWait constructwaiting");
t2.Start();
Thread.Sleep(5);
_isCompleted = true;
- 运行程序。
工作原理...
当主程序启动时,它定义了一个线程,该线程将执行一个无限循环,每 20 毫秒一次,直到主线程将_isCompleted
变量设置为true
。我们可以尝试将这个循环运行 20-30 秒,使用 Windows 任务管理器来测量 CPU 负载。这将显示出相当大量的处理器时间,取决于 CPU 有多少个核心。
我们使用volatile
关键字来声明_isCompleted
静态字段。volatile
关键字表示一个字段可能会被多个线程同时修改。声明为volatile
的字段不受编译器和处理器优化的影响,这些优化假定只有一个线程访问。这确保了字段中始终存在最新的值。
然后我们使用SpinWait
版本,每次迭代都打印一个特殊的标志,显示线程是否将切换到阻塞状态。我们运行这个线程 5 毫秒来观察。在开始时,SpinWait
试图保持在用户模式下,大约经过九次迭代后,它开始将线程切换到阻塞状态。如果我们尝试使用这个版本来测量 CPU 负载,我们将在 Windows 任务管理器中看不到任何 CPU 使用率。
第三章:使用线程池
在本章中,我们将描述使用多个线程从共享资源中工作的常见技术。您将了解:
-
在线程池上调用委托
-
在线程池上发布异步操作
-
线程池和并行度
-
实现取消选项
-
使用等待句柄和线程池的超时
-
使用定时器
-
使用 BackgroundWorker 组件
介绍
在前几章中,我们讨论了创建线程和组织它们的合作的几种方法。现在让我们考虑另一种情况,即创建许多需要很短时间完成的异步操作。正如我们在第一章的介绍部分中讨论的那样,创建线程是一项昂贵的操作,因此为每个短暂的异步操作进行这样的操作将包含显着的开销。
为了解决这个问题,有一种称为池化的常见方法,可以成功应用于任何需要许多短暂的昂贵资源的情况。我们预先分配一定数量的这些资源,并将它们组织成资源池。每当我们需要新资源时,我们只需从池中取出,而不是创建一个新的,并在资源不再需要时将其返回到池中。
.NET 线程池是这个概念的一种实现。它可以通过System.Threading.ThreadPool
类型访问。线程池由.NET 公共语言运行时(CLR)管理,这意味着每个 CLR 都有一个线程池实例。ThreadPool
类型有一个QueueUserWorkItem
静态方法,接受一个代表用户定义的异步操作的委托。调用此方法后,该委托进入内部队列。然后,如果线程池中没有线程,它会创建一个新的工作线程,并将第一个委托放入队列中。
如果我们在线程池中放置新操作,那么在前面的操作完成后,可以重用这个线程来执行这些操作。但是,如果我们更快地放置新操作,线程池将创建更多线程来为这些操作提供服务。有一个限制来防止创建太多的线程,在这种情况下,新操作将在队列中等待,直到线程池中的工作线程空闲为止。
注意
非常重要的是,要保持线程池中的操作生命周期短暂!不要将长时间运行的操作放在线程池中或阻塞工作线程。这将导致所有工作线程都变得忙碌,它们将无法再为用户操作提供服务。这反过来会导致性能问题和非常难以调试的错误。
当我们停止在线程池上放置新操作时,它最终会删除不再需要的线程,这些线程在一段时间后处于空闲状态。这将释放不再需要的任何操作系统资源。
我想再次强调,线程池旨在执行短期操作。使用线程池可以节省操作系统资源,但会降低并行度。我们使用较少的线程,但执行异步操作比通常慢,通过可用的工作线程数量进行批处理。如果操作完成速度很快,这是有意义的,但对于执行许多长时间运行的计算密集型操作,性能会下降。
另一个需要非常小心的重要事情是在 ASP.NET 应用程序中使用线程池。ASP.NET 基础设施本身使用线程池,如果浪费了所有线程池的工作线程,Web 服务器将无法再服务传入的请求。建议在 ASP.NET 中只使用输入/输出绑定的异步操作,因为它们使用一种称为 I/O 线程的不同机制。我们将在第九章中讨论 I/O 线程,使用异步 I/O。
注意
请注意,线程池的工作线程是后台线程。这意味着当前台中的所有线程(包括主应用程序线程)完成后,所有后台线程将停止。
在本章中,我们将学习如何使用线程池执行异步操作。我们将涵盖将操作放在线程池上的不同方法,以及如何取消操作并防止其长时间运行。
在线程池上调用委托
这个配方将向你展示如何在线程池上异步执行委托。此外,我们将讨论一种称为异步编程模型(APM)的方法,这是.NET 中历史上第一种异步编程模式。
准备工作
进入这个配方,你需要 Visual Studio 2012。没有其他先决条件。这个配方的源代码可以在BookSamples\Chapter3\Recipe1
中找到
如何做...
要了解如何在线程池上调用委托,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
private delegate string RunOnThreadPool(out int threadId);
private static void Callback(IAsyncResultar)
{
Console.WriteLine("Starting a callback...");
Console.WriteLine("State passed to a callback: {0}",ar.AsyncState);
Console.WriteLine("Is thread pool thread: {0}",Thread.CurrentThread.IsThreadPoolThread);
Console.WriteLine("Thread pool worker thread id: {0}",Thread.CurrentThread.ManagedThreadId);
}
private static string Test(out intthreadId)
{
Console.WriteLine("Starting...");
Console.WriteLine("Is thread pool thread: {0}",Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(2));
threadId = Thread.CurrentThread.ManagedThreadId;
return string.Format("Thread pool worker thread id was:{0}", threadId);
}
- 在
Main
方法中添加以下代码:
int threadId = 0;
RunOnThreadPool poolDelegate = Test;
var t = new Thread(() => Test(out threadId));
t.Start();
t.Join();
Console.WriteLine("Thread id: {0}", threadId);
IAsyncResult r = poolDelegate.BeginInvoke(out threadId,Callback, "a delegate asynchronous call");
r.AsyncWaitHandle.WaitOne();
string result = poolDelegate.EndInvoke(out threadId, r);
Console.WriteLine("Thread pool worker thread id: {0}",threadId);
Console.WriteLine(result);
Thread.Sleep(TimeSpan.FromSeconds(2));
- 运行程序。
它是如何工作的...
程序运行时,以一种老式的方式创建一个线程,然后启动它并等待其完成。由于线程构造函数只接受一个不返回任何结果的方法,我们使用lambda 表达式来包装对Test
方法的调用。我们确保这个线程不是来自线程池,通过打印Thread.CurrentThread.IsThreadPoolThread
属性值。我们还打印一个托管线程 ID 来标识执行此代码的线程。
然后我们定义一个委托,并通过调用BeginInvoke
方法来运行它。这个方法接受一个回调函数,在异步操作完成后将被调用,以及一个用户定义的状态传递到回调函数中。这个状态通常用于区分一个异步调用和另一个。结果,我们得到一个实现IAsyncResult
接口的result
对象。BeginInvoke
立即返回结果,允许我们在异步操作在线程池的工作线程上执行时继续进行任何工作。当我们需要异步操作的结果时,我们使用从BeginInvoke
方法调用返回的result
对象。我们可以使用result
属性IsCompleted
进行轮询,但在这种情况下,我们使用AsyncWaitHandle
结果属性来等待,直到操作完成。完成后,要从中获取结果,我们在委托上调用EndInvoke
方法,传递委托参数和我们的IAsyncResult
对象。
注意
实际上,使用AsyncWaitHandle
是不必要的。如果我们注释掉r.AsyncWaitHandle.WaitOne
,代码仍然会成功运行,因为EndInvoke
方法实际上会等待异步操作完成。始终重要的是调用EndInvoke
(或对于其他异步 API,调用EndOperationName
),因为它会将任何未处理的异常抛回到调用线程。在使用这种类型的异步 API 时,始终调用Begin
和End
方法。
当操作完成时,传递给BeginInvoke
方法的回调将被发布到线程池,更具体地说,是一个工作线程。如果我们在Main
方法定义的末尾注释掉Thread.Sleep
方法调用,回调将不会被执行。这是因为当主线程完成时,所有后台线程都将被停止,包括这个回调。可能会有两个异步调用委托和一个回调将由同一个工作线程提供服务,这很容易通过工作线程 ID 看到。
在.NET 中使用BeginOperationName
/EndOperationName
方法和IAsyncResult
对象的方法被称为异步编程模型或 APM 模式,这样的方法对被称为异步方法。这种模式仍然被用于各种.NET 类库 API 中,但在现代编程中,最好使用任务并行库(TPL)来组织异步 API。我们将在第四章中涵盖这个主题,使用任务并行库。
在线程池上发布异步操作
这个配方将描述如何将异步操作放在线程池中。
准备就绪
要进入这个配方,你需要 Visual Studio 2012。没有其他先决条件。这个配方的源代码可以在BookSamples\Chapter3\Recipe2
中找到。
如何做...
要理解如何将异步操作发布到线程池,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
usingSystem.Threading;
- 在
Main
方法下面添加以下代码片段:
private static void AsyncOperation(object state)
{
Console.WriteLine("Operation state: {0}",state ?? "(null)");
Console.WriteLine("Worker thread id: {0}",Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromSeconds(2));
}
- 在
Main
方法内部添加以下代码片段:
const int x = 1;
const int y = 2;
const string lambdaState = "lambda state 2";
ThreadPool.QueueUserWorkItem(AsyncOperation);
Thread.Sleep(TimeSpan.FromSeconds(1));
ThreadPool.QueueUserWorkItem(AsyncOperation,"async state");
Thread.Sleep(TimeSpan.FromSeconds(1));
ThreadPool.QueueUserWorkItem( state => {
Console.WriteLine("Operation state: {0}", state);
Console.WriteLine("Worker thread id: {0}",Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromSeconds(2));
}, "lambda state");
ThreadPool.QueueUserWorkItem( _ => {
Console.WriteLine("Operation state: {0}, {1}", x+y,lambdaState);
Console.WriteLine("Worker thread id: {0}",Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromSeconds(2));
}, "lambda state");
Thread.Sleep(TimeSpan.FromSeconds(2));
- 运行程序。
它是如何工作的...
首先,我们定义了接受一个对象类型参数的AsyncOperation
方法。然后,我们使用QueueUserWorkItem
方法将这个方法发布到线程池。然后我们再次发布这个方法,但这次我们将一个state
对象传递给这个方法调用。这个对象将作为state
参数传递给AsynchronousOperation
方法。
在这些操作之后让一个线程休眠 1 秒,为线程池提供重用线程的可能性进行新的操作。如果你注释掉这些Thread.Sleep
调用,几乎可以肯定,所有情况下线程 ID 都会不同。如果不是,可能前两个线程将被重用来运行接下来的两个操作。
首先,我们将一个 lambda 表达式发布到线程池。这里没有什么特别的;我们使用 lambda 表达式语法来定义一个单独的方法。
其次,我们不是传递 lambda 表达式的状态,而是使用闭包机制。这给了我们更多的灵活性,并允许我们为异步操作提供多个对象和这些对象的静态类型。因此,以前将对象传递给方法回调的机制实际上是多余和过时的。现在当我们在 C#中有闭包时,就没有必要使用它了。
线程池和并行度
这个配方将展示线程池如何处理许多异步操作,以及它与创建许多单独线程的不同之处。
准备就绪
要进入这个配方,你需要 Visual Studio 2012。没有其他先决条件。这个配方的源代码可以在BookSamples\Chapter3\Recipe3
中找到。
如何做...
要了解线程池如何处理许多异步操作以及它与创建许多单独线程的不同之处,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Diagnostics;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void UseThreads(int numberOfOperations)
{
using (var countdown = new CountdownEvent(numberOfOperations)) {
Console.WriteLine("Scheduling work by creatingthreads");
for (int i=0; i<numberOfOperations; i++) {
var thread = new Thread(() => {
Console.Write("{0},", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromSeconds(0.1));
countdown.Signal();
});
thread.Start();
}
countdown.Wait();
Console.WriteLine();
}
}
static void UseThreadPool(int numberOfOperations)
{
using (var countdown = new CountdownEvent(numberOfOperations)) {
Console.WriteLine("Starting work on a threadpool");
for (int i=0; i<numberOfOperations; i++) {
ThreadPool.QueueUserWorkItem( _ => {
Console.Write("{0},", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(TimeSpan.FromSeconds(0.1));
countdown.Signal();
});
}
countdown.Wait();
Console.WriteLine();
}
}
- 在
Main
方法内部添加以下代码片段:
const int numberOfOperations = 500;
var sw = new Stopwatch();
sw.Start();
UseThreads(numberOfOperations);
sw.Stop();
Console.WriteLine("Execution time using threads: {0}",sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
UseThreadPool(numberOfOperations);
sw.Stop();
Console.WriteLine("Execution time using threads: {0}",sw.ElapsedMilliseconds);
- 运行程序。
它是如何工作的...
当主程序启动时,我们创建许多不同的线程,并在每个线程上运行一个操作。这个操作打印出一个线程 ID,并阻塞一个线程 100 毫秒。结果,我们创建了 500 个线程,它们都并行运行这些操作。在我的机器上,总时间约为 300 毫秒,但我们消耗了许多操作系统资源来运行所有这些线程。
然后,我们按照相同的程序进行,但是不是为每个操作创建一个线程,而是将它们发布到线程池上。之后,线程池开始为这些操作提供服务;它在最后开始创建更多的线程,但仍然需要更多的时间,大约在我的机器上需要 12 秒。我们节省了内存和线程供操作系统使用,但为此付出了执行时间。
实现取消选项
在这个示例中,有一个关于如何在线程池上取消异步操作的示例。
准备就绪
要进入这个示例,您需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter3\Recipe4
中找到。
如何做...
要了解如何在线程上实现取消选项,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void AsyncOperation1(CancellationToken token) {
Console.WriteLine("Starting the first task");
for (int i=0; i<5; i++) {
if (token.IsCancellationRequested) {
Console.WriteLine("The first task has beencanceled.");
return;
}
Thread.Sleep(TimeSpan.FromSeconds(1));
}
Console.WriteLine("The first task has completedsuccesfully");
}
static void AsyncOperation2(CancellationToken token) {
try {
Console.WriteLine("Starting the second task");
for (int i=0; i<5; i++) {
token.ThrowIfCancellationRequested();
Thread.Sleep(TimeSpan.FromSeconds(1));
}
Console.WriteLine("The second task has completedsuccessfully");
}
catch (OperationCanceledException) {
Console.WriteLine("The second task has beencanceled.");
}
}
private static void AsyncOperation3(CancellationToken token) {
boolcancellationFlag = false;
token.Register(()=>cancellationFlag=true);
Console.WriteLine("Starting the third task");
for (int i=0; i<5; i++) {
if (cancellationFlag) {
Console.WriteLine("The third task has beencanceled.");
return;
}
Thread.Sleep(TimeSpan.FromSeconds(1));
}
Console.WriteLine("The third task has completedsuccesfully");
}
- 在
Main
方法内添加以下代码片段:
using (var cts = new CancellationTokenSource()) {
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => AsyncOperation1(token));
Thread.Sleep(TimeSpan.FromSeconds(2));
cts.Cancel();
}
using (var cts = new CancellationTokenSource()) {
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => AsyncOperation2(token));
Thread.Sleep(TimeSpan.FromSeconds(2));
cts.Cancel();
}
using (var cts = new CancellationTokenSource()) {
CancellationToken token = cts.Token;
ThreadPool.QueueUserWorkItem(_ => AsyncOperation3(token));
Thread.Sleep(TimeSpan.FromSeconds(2));
cts.Cancel();
}
Thread.Sleep(TimeSpan.FromSeconds(2));
- 运行程序。
它是如何工作的...
在这里,我们引入了新的CancellationTokenSource
和CancellationToken
构造。它们出现在.NET 4.0 中,现在已经成为实现异步操作取消过程的事实标准。由于线程池已经存在很长时间,它没有专门的 API 用于取消标记;然而,它们仍然可以使用。
在这个程序中,我们看到了三种组织取消过程的方法。第一种方法是轮询和检查CancellationToken.IsCancellationRequested
属性。如果它被设置为true
,这意味着我们的操作正在被取消,我们必须放弃这个操作。
第二种方法是抛出OperationCancelledException
异常。这允许从被取消的操作内部控制取消过程,而不是从外部代码控制。
最后的选择是在线程池上注册一个回调,当操作被取消时将在线程池上调用。这将允许将取消逻辑链接到另一个异步操作中。
使用等待句柄和线程池的超时
本示例将描述如何为线程池操作实现超时,以及如何在线程池上正确等待。
准备就绪
要进入这个示例,您需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter3\Recipe5
中找到。
如何做...
要学习如何实现超时以及如何在线程池上正确等待,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下面添加以下代码片段:
static void RunOperations(TimeSpanworkerOperationTimeout) {
using (var evt = new ManualResetEvent(false))
using (var cts = new CancellationTokenSource()) {
Console.WriteLine("Registering timeout operations...");
var worker = ThreadPool.RegisterWaitForSingleObject(evt, (state, isTimedOut) => WorkerOperationWait(cts,isTimedOut), null, workerOperationTimeout, true);
Console.WriteLine("Starting long runningoperation...");
ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt));
Thread.Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2)));
worker.Unregister(evt);
}
}
static void WorkerOperation(CancellationToken token,ManualResetEventevt) {
for(int i=0; i<6; i++) {
if (token.IsCancellationRequested) {
return;
}
Thread.Sleep(TimeSpan.FromSeconds(1));
}
evt.Set();
}
static void WorkerOperationWait(CancellationTokenSource ctsbool isTimedOut) {
if (isTimedOut) {
cts.Cancel();
Console.WriteLine("Worker operation timed out and wascanceled.");
}
else {
Console.WriteLine("Worker operation succeded.");
}
}
- 在
Main
方法内添加以下代码片段:
RunOperations(TimeSpan.FromSeconds(5));
RunOperations(TimeSpan.FromSeconds(7));
- 运行程序。
它是如何工作的...
线程池还有另一个有用的方法:ThreadPool.RegisterWaitForSingleObject
。这个方法允许我们在线程池上排队一个回调,当提供的等待句柄被信号或超时发生时,这个回调将被执行。这允许我们为线程池操作实现超时。
首先,我们在线程池上排队一个长时间运行的操作。它运行了 6 秒,然后设置了一个ManualResetEvent
信号构造,以防它成功完成。在其他情况下,如果请求取消,操作就会被放弃。
然后,我们注册第二个异步操作,当它从ManualResetEvent
对象接收到信号时将被调用,该对象由第一个操作设置,如果第一个操作成功完成。另一个选项是在第一个操作完成之前发生超时。如果发生这种情况,我们使用CancellationToken
来取消第一个操作。
最后,如果我们为操作提供了 5 秒的超时,这是不够的。这是因为操作需要 6 秒才能完成,我们需要取消这个操作。所以如果我们提供了一个 7 秒的超时,这是可以接受的,操作将成功完成。
还有更多...
当您有大量的线程必须在阻塞状态下等待某个多线程事件构造发出信号时,这是非常有用的。我们可以使用线程池基础设施,而不是阻塞所有这些线程。它将允许释放这些线程,直到事件被设置。这对于需要可伸缩性和性能的服务器应用程序来说是一个非常重要的场景。
使用定时器
这个食谱将描述如何使用System.Threading.Timer
对象在线程池上创建周期调用的异步操作。
准备工作
要进入这个食谱,您将需要 Visual Studio 2012。没有其他先决条件。这个食谱的源代码可以在BookSamples\Chapter3\Recipe6
中找到。
如何做...
要学习如何在线程池上创建周期调用的异步操作,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
- 在
Main
方法下方添加以下代码片段:
static Timer _timer;
static void TimerOperation(DateTime start) {
TimeSpan elapsed = DateTime.Now - start;
Console.WriteLine("{0} seconds from {1}. Timer threadpool thread id: {2}", elapsed.Seconds, start,
Thread.CurrentThread.ManagedThreadId);
}
- 在
Main
方法中添加以下代码片段:
Console.WriteLine("Press 'Enter' to stop the timer...");
DateTime start = DateTime.Now;
_timer = new Timer(_ => TimerOperation(start), null,TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));
Thread.Sleep(TimeSpan.FromSeconds(6));
_timer.Change(TimeSpan.FromSeconds(1),TimeSpan.FromSeconds(4));
Console.ReadLine();
_timer.Dispose();
- 运行程序。
它是如何工作的...
首先,我们创建一个新的Timer
实例。第一个参数是一个将在线程池上执行的 lambda 表达式。我们调用TimerOperation
方法,提供一个开始日期。我们不使用用户state
对象,所以第二个参数是 null;然后,我们指定何时第一次运行TimerOperation
,以及调用之间的时间间隔。因此,第一个值实际上意味着我们在一秒钟内开始第一个操作,然后每个操作运行 2 秒。
之后,我们等待 6 秒并改变我们的定时器。我们在调用_timer.Change
方法后的一秒钟开始TimerOperation
,然后每个运行 4 秒。
提示
定时器可能比这更复杂!
可以以更复杂的方式使用定时器。例如,我们可以仅运行定时器操作一次,提供一个Timeout.Infinte
值的定时器周期参数。然后,在定时器异步操作中,我们能够根据一些自定义逻辑设置下一次定时器操作将被执行的时间。
最后,我们等待Enter键被按下并完成应用程序。当它运行时,我们可以看到自程序启动以来经过的时间。
使用 BackgroundWorker 组件
这个食谱描述了另一种异步编程的方法,以BackgroundWorker
组件为例。借助这个对象,我们能够将异步代码组织成一组事件和事件处理程序。您将学习如何使用这个组件进行异步编程。
准备工作
要进入这个食谱,您将需要 Visual Studio 2012。没有其他先决条件。这个食谱的源代码可以在BookSamples\Chapter3\Recipe7
中找到。
如何做...
要学习如何使用BackgroundWorker
组件,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.ComponentModel;
using System.Threading;
- 在
Main
方法下方添加以下代码片段:
static void Worker_DoWork(object sender, DoWorkEventArgs e)
{
Console.WriteLine("DoWork thread pool thread id: {0}",Thread.CurrentThread.ManagedThreadId);
var bw = (BackgroundWorker) sender;
for (int i=1; i<=100; i++) {
if (bw.CancellationPending) {
e.Cancel = true;
return;
}
if (i%10 == 0) {
bw.ReportProgress(i);
}
Thread.Sleep(TimeSpan.FromSeconds(0.1));
}
e.Result = 42;
}
static void Worker_ProgressChanged(object sender,ProgressChangedEventArgs e){
Console.WriteLine("{0}% completed. Progress thread poolthread id: {1}", e.ProgressPercentage, Thread.CurrentThread.ManagedThreadId);
}
static void Worker_Completed(object sender,RunWorkerCompletedEventArgs e) {
Console.WriteLine("Completed thread pool thread id: {0}",Thread.CurrentThread.ManagedThreadId);
if (e.Error != null) {
Console.WriteLine("Exception {0} has occured.",e.Error.Message);
}
else if (e.Cancelled) {
Console.WriteLine("Operation has been canceled.");
}
else {
Console.WriteLine("The answer is: {0}", e.Result);
}
}
- 在
Main
方法中添加以下代码片段:
var bw = new BackgroundWorker();
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.DoWork += Worker_DoWork;
bw.ProgressChanged += Worker_ProgressChanged;
bw.RunWorkerCompleted += Worker_Completed;
bw.RunWorkerAsync();
Console.WriteLine("Press C to cancel work");
do {
if (Console.ReadKey(true).KeyChar == 'C') {
bw.CancelAsync();
}
}
while(bw.IsBusy);
- 运行程序。
它是如何工作的...
程序启动时,我们创建了一个BackgroundWorker
组件的实例。我们明确表示我们希望我们支持后台工作的操作取消和操作进度的通知。
现在,最有趣的部分出现了。与其使用线程池和委托进行操作,我们使用另一个 C#习语,称为事件。事件代表某种通知的一个源,以及一些准备好在通知到达时做出反应的订阅者。在我们的情况下,我们声明我们将订阅三个事件,当它们发生时,我们将调用相应的事件处理程序。这些是具有特别定义签名的方法,当事件通知其订阅者时将被调用。
因此,与其在一对Begin
/End
方法中组织异步 API,不如只是启动异步操作,然后订阅在执行此操作时可能发生的不同事件。这种方法被称为基于事件的异步模式(EAP)。这在历史上是对异步程序进行结构化的第二次尝试,现在建议使用 TPL,这将在第四章中描述,使用任务并行库。
因此,我们已经订阅了三个事件。其中之一是DoWork
事件。当后台工作程序对象使用RunWorkerAsync
方法开始异步操作时,将调用此事件的处理程序。事件处理程序将在线程池上执行,这是主要的操作点,在这里,如果请求取消,则工作将被取消,并且我们提供操作的进度信息。最后,当我们获得结果时,我们将其设置为事件参数,然后调用RunWorkerCompleted
事件处理程序。在此方法内部,我们会找出我们的操作是否成功,或者可能出现了一些错误,或者它被取消了。
此外,BackgroundWorker
组件实际上是用于Windows 窗体应用程序(WPF)。它的实现使得可以直接从后台工作程序事件处理程序的代码中使用 UI 控件,这与线程池的工作线程与 UI 控件的交互相比非常方便。
第四章:使用任务并行库
在本章中,我们将深入研究一种新的异步编程范式,任务并行库。您将学习以下内容:
-
创建任务
-
执行任务的基本操作
-
将任务组合在一起
-
将 APM 模式转换为任务
-
将 EAP 模式转换为任务
-
实现取消选项
-
处理任务中的异常
-
并行运行任务
-
使用 TaskScheduler 调整任务执行
介绍
在之前的章节中,我们学习了什么是线程,如何使用线程,以及为什么我们需要线程池。使用线程池允许我们节省操作系统资源,但代价是降低了并行度。我们可以将线程池视为一个抽象层,它将线程使用的细节隐藏起来,使我们能够集中精力在程序逻辑上,而不是线程问题上。
然而,使用线程池也是复杂的。没有简单的方法从线程池工作线程中获取结果。我们需要实现自己的方法来获取结果,并且在发生异常时,我们必须正确地将其传播到原始线程。除此之外,没有简单的方法来创建一组依赖的异步操作,其中一个操作在另一个操作完成后运行。
有几次尝试解决这些问题,结果产生了异步编程模型和基于事件的异步模式,这在第三章使用线程池中提到。这些模式使得获取结果更容易,并且在传播异常方面做得很好,但是将异步操作组合在一起仍然需要大量的工作,并且导致了大量的代码。
为了解决所有这些问题,在.Net Framework 4.0 中引入了一种新的用于异步操作的 API。它被称为任务并行库(TPL)。它在.Net Framework 4.5 中略有改变,为了更清楚起见,我们将在我们的项目中使用.Net Framework 4.5 版本来使用最新版本的 TPL。TPL 可以被视为线程池上的另一种抽象层,它隐藏了与线程池一起工作的底层代码,使程序员无需关注,并提供了更方便和细粒度的 API。
TPL 的核心概念是任务。任务代表一个异步操作,可以以各种方式运行,使用单独的线程或不使用。我们将在本章中详细讨论所有可能性。
注意
默认情况下,程序员不知道任务的执行方式。TPL 通过隐藏任务的实现细节,提高了抽象级别。不幸的是,在某些情况下,这可能导致神秘的错误,比如在尝试从任务中获取结果时挂起应用程序。本章将帮助理解 TPL 底层的机制,以及如何避免以不当的方式使用它。
任务可以以不同的方式与其他任务组合。例如,我们可以同时启动几个任务,等待它们全部完成,然后运行一个任务,对所有先前任务的结果进行一些计算。与以前的模式相比,任务组合的便利 API 是 TPL 的关键优势之一。
还有几种处理任务异常的方法。由于一个任务可能由几个其他任务组成,它们又有自己的子任务,因此有一个AggregateException
的概念。这种类型的异常包含了所有底层任务的异常,允许单独处理它们。
最后但并非最不重要的是,C# 5.0 内置了对 TPL 的支持,允许我们使用新的await
和async
关键字以非常流畅和舒适的方式处理任务。我们将在第五章使用 C# 5.0中讨论这个话题。
在本章中,我们将学习使用 TPL 执行异步操作。我们将学习任务是什么,覆盖创建任务的不同方式,以及如何将任务组合在一起。我们还将讨论如何将传统的 APM 和 EAP 模式转换为使用任务,如何正确处理异常,如何取消任务,以及如何同时处理多个任务。此外,我们将了解如何正确处理 Windows GUI 应用程序中的任务。
创建任务
这个配方展示了任务的基本概念。您将学习如何创建和执行任务。
准备工作
要按照这个配方进行,您将需要Visual Studio 2012。没有其他先决条件。这个配方的源代码可以在BookSamples\Chapter4\Recipe1
中找到。
如何操作...
要创建和执行任务,请执行以下步骤:
- 启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
注意
这次,请确保您使用的是.Net Framework 4.5。从现在开始,我们将为每个项目使用这个版本。
- 在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static void TaskMethod(string name){
Console.WriteLine("Task {0} is running on a thread id{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
}
- 在
Main
方法内添加以下代码片段:
var t1 = new Task(() =>TaskMethod("Task 1"));
var t2 = new Task(() =>TaskMethod("Task 2"));
t2.Start();
t1.Start();
Task.Run(() =>TaskMethod("Task 3"));
Task.Factory.StartNew(() => TaskMethod("Task 4"));
Task.Factory.StartNew(() => TaskMethod("Task 5"),TaskCreationOptions.LongRunning);
Thread.Sleep(TimeSpan.FromSeconds(1));
- 运行程序。
它是如何工作的...
当程序运行时,它使用构造函数创建两个任务。我们将 lambda 表达式作为Action
委托传递;这允许我们向TaskMethod
提供一个字符串参数。然后,我们使用Start
方法运行这些任务。
注意
请注意,在调用这些任务的Start
方法之前,它们不会开始执行。很容易忘记实际启动任务。
然后,我们使用Task.Run
和Task.Factory.StartNew
方法运行另外两个任务。不同之处在于,创建的任务立即开始工作,因此我们不需要在任务上显式调用Start
方法。所有任务,从Task 1
到Task 4
,都放置在线程池工作线程上,并以未指定的顺序运行。如果多次运行程序,您会发现任务的执行顺序是不确定的。
Task.Run
方法只是Task.Factory.StartNew
的快捷方式,但后者有额外的选项。一般情况下,除非需要做一些特殊的事情,如Task 5
的情况,否则使用前者方法。我们将这个任务标记为长时间运行,结果,这个任务将在一个单独的线程上运行,而不使用线程池。然而,这种行为可能会改变,取决于当前运行任务的任务调度程序。您将在本章的最后一个配方中了解什么是任务调度程序。
执行任务的基本操作
这个配方将描述如何从任务中获取结果值。我们将通过几种情景来理解在线程池或主线程上运行任务的区别。
准备工作
要开始这个配方,您将需要 Visual Studio 2012。没有其他先决条件。这个配方的源代码可以在BookSamples\Chapter4\Recipe2
中找到。
如何操作...
要执行任务的基本操作,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static Task<int>CreateTask(string name){
return new Task<int>(() =>TaskMethod(name));
}
static int TaskMethod(string name){
Console.WriteLine("Task {0} is running on a thread id{1}. Is thread pool thread: {2}",name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(2));
return 42;
}
- 在
Main
方法内添加以下代码片段:
TaskMethod("Main Thread Task");
Task<int> task = CreateTask("Task 1");
task.Start();
int result = task.Result;
Console.WriteLine("Result is: {0}", result);
task = CreateTask("Task 2");
task.RunSynchronously();
result = task.Result;
Console.WriteLine("Result is: {0}", result);
task = CreateTask("Task 3");
task.Start();
while (!task.IsCompleted){
Console.WriteLine(task.Status);
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}
Console.WriteLine(task.Status);
result = task.Result;
Console.WriteLine("Result is: {0}", result);
- 运行程序。
它是如何工作的...
首先,我们运行TaskMethod
,而不将其包装成任务。结果,它是同步执行的,为我们提供了关于主线程的信息。显然,这不是一个线程池线程。
然后我们运行Task 1
,使用Start
方法启动它并等待结果。这个任务将放在线程池上,主线程会等待并被阻塞,直到任务返回。
我们对Task 2
做同样的操作,只是我们使用RunSynchronously()
方法来运行它。这个任务将在主线程上运行,我们得到的输出与当我们只是同步调用TaskMethod
时完全相同。这是一个非常有用的优化,允许我们避免对非常短暂的操作使用线程池。
我们以与Task 1
相同的方式运行Task 3
,但是不阻塞主线程,只是旋转,打印出任务状态,直到任务完成。这显示了几个任务状态,分别是Created
,Running
和RanToCompletion
。
将任务组合在一起
这个示例将展示如何设置相互依赖的任务。我们将学习如何创建一个任务,在父任务完成后运行。此外,我们将发现一种节省线程使用的可能性,用于非常短暂的任务。
准备工作
要执行此示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter4\Recipe3
中找到。
如何做...
要将任务组合在一起,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# Console Application项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static int TaskMethod(string name, int seconds){
Console.WriteLine("Task {0} is running on a thread id
{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
return 42 * seconds;
}
- 在
Main
方法内部添加以下代码片段:
var firstTask = new Task<int>(() =>TaskMethod("First Task",3));
var secondTask = new Task<int>(() =>TaskMethod("SecondTask", 2));
firstTask.ContinueWith(
t =>Console.WriteLine("The first answer is {0}. Thread id{1}, is thread pool thread: {2}", t.Result,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread),TaskContinuationOptions.OnlyOnRanToCompletion);
firstTask.Start();
secondTask.Start();
Thread.Sleep(TimeSpan.FromSeconds(4));
Task continuation = secondTask.ContinueWith(
t =>Console.WriteLine("The second answer is {0}. Threadid {1}, is thread pool thread: {2}", t.Result,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread),TaskContinuationOptions.OnlyOnRanToCompletion |TaskContinuationOptions.ExecuteSynchronously);
continuation.GetAwaiter().OnCompleted(
() =>Console.WriteLine("Continuation Task Completed!Thread id {0}, is thread pool thread: {1}",Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread));
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine();
firstTask = new Task<int>(() => {varinnerTask = Task.Factory.StartNew(() =>TaskMethod("Second Task", 5), TaskCreationOptions.AttachedToParent);
innerTask.ContinueWith(t =>TaskMethod("Third Task", 2),TaskContinuationOptions.AttachedToParent);
return TaskMethod("First Task", 2);
});
firstTask.Start();
while (!firstTask.IsCompleted){
Console.WriteLine(firstTask.Status);
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}
Console.WriteLine(firstTask.Status);
Thread.Sleep(TimeSpan.FromSeconds(10));
- 运行程序。
它是如何工作的...
当主程序启动时,我们创建两个任务,对于第一个任务,我们设置了一个continuation(在前一个任务完成后运行的代码块)。然后我们启动这两个任务并等待 4 秒,这足够让两个任务都完成。然后我们对第二个任务运行另一个 continuation,并尝试通过指定TaskContinuationOptions.ExecuteSynchronously
选项同步执行它。当 continuation 非常短暂时,这是一种有用的技术,它将更快地在主线程上运行而不是放在线程池中。我们能够做到这一点是因为第二个任务在那时已经完成。如果我们注释掉 4 秒的Thread.Sleep
方法,我们将看到这段代码将被放在线程池中,因为我们还没有从前一个任务得到结果。
最后,我们以稍微不同的方式为前一个 continuation 定义一个 continuation,使用新的GetAwaiter
和OnCompleted
方法。这些方法旨在与 C# 5.0 语言的异步机制一起使用。我们将在第五章中详细介绍这个主题,使用 C# 5.0。
演示的最后部分是关于父子任务关系。我们创建一个新任务,同时运行这个任务,通过提供TaskCreationOptions.AttachedToParent
选项来运行所谓的子任务。
提示
在运行父任务时必须创建子任务以正确附加到父任务!
这意味着父任务不会完成直到所有子任务完成其工作。我们还能够在子任务上运行 continuations,提供TaskContinuationOptions.AttachedToParent
选项。这个 continuation 也会影响父任务,并且直到最后一个子任务结束才会完成。
将 APM 模式转换为任务
在这个示例中,我们将看到如何将老式的 APM API 转换为任务。有不同情况的示例可能发生在转换过程中。
准备工作
要开始这个示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter4\Recipe4
中找到。
如何做...
要将 APM 模式转换为任务,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# Console Application项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
private delegate string AsynchronousTask(stringthreadName);
private delegate string IncompatibleAsynchronousTask(outint threadId);
private static void Callback(IAsyncResultar){
Console.WriteLine("Starting a callback...");
Console.WriteLine("State passed to a callback: {0}",ar.AsyncState);
Console.WriteLine("Is thread pool thread: {0}",Thread.CurrentThread.IsThreadPoolThread);
Console.WriteLine("Thread pool worker thread id: {0}",Thread.CurrentThread.ManagedThreadId);
}
private static string Test(string threadName){
Console.WriteLine("Starting...");
Console.WriteLine("Is thread pool thread: {0}",Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(2));
Thread.CurrentThread.Name = threadName;
return string.Format("Thread name: {0}",Thread.CurrentThread.Name);
}
private static string Test(out int threadId){
Console.WriteLine("Starting...");
Console.WriteLine("Is thread pool thread: {0}",Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(2));
threadId = Thread.CurrentThread.ManagedThreadId;
return string.Format("Thread pool worker thread id was:{0}", threadId);
}
- 在
Main
方法内部添加以下代码片段:
int threadId;
AsynchronousTask d = Test;
IncompatibleAsynchronousTask e = Test;
Console.WriteLine("Option 1");
Task<string> task = Task<string>.Factory.FromAsync(
d.BeginInvoke("AsyncTaskThread", Callback, "a delegateasynchronous call"), d.EndInvoke);
task.ContinueWith(t =>Console.WriteLine("Callback isfinished, now running a continuation! Result: {0}",t.Result));
while (!task.IsCompleted){
Console.WriteLine(task.Status);
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}
Console.WriteLine(task.Status);
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("----------------------------------------");
Console.WriteLine();
Console.WriteLine("Option 2");
task = Task<string>.Factory.FromAsync(
d.BeginInvoke, d.EndInvoke, "AsyncTaskThread", "adelegate asynchronous call");
task.ContinueWith(t =>Console.WriteLine("Task is completed,now running a continuation! Result: {0}",t.Result));
while (!task.IsCompleted){
Console.WriteLine(task.Status);
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}
Console.WriteLine(task.Status);
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine("------------------------------------------");
Console.WriteLine();
Console.WriteLine("Option 3");
IAsyncResult ar = e.BeginInvoke(out threadId, Callback, "adelegate asynchronous call");
ar = e.BeginInvoke(out threadId, Callback, "a delegateasynchronous call");
task = Task<string>.Factory.FromAsync(ar, _ =>e.EndInvoke(out threadId, ar));
task.ContinueWith(t =>
Console.WriteLine("Task is completed, now running acontinuation! Result: {0}, ThreadId: {1}",t.Result, threadId));
while (!task.IsCompleted){
Console.WriteLine(task.Status);
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}
Console.WriteLine(task.Status);
Thread.Sleep(TimeSpan.FromSeconds(1));
- 运行程序。
工作原理...
在这里,我们定义了两种类型的委托;其中一种使用了out
参数,因此与将 APM 模式转换为任务的标准 TPL API 不兼容。然后我们有三个这样转换的示例。
将 APM 转换为 TPL 的关键点是Task<T>.Factory.FromAsync
方法,其中T
是异步操作的结果类型。该方法有几种重载;在第一种情况下,我们传递IAsyncResult
和Func<IAsyncResult, string>
,这是一个接受IAsyncResult
实现并返回一个字符串的方法。由于第一个委托类型提供了与此签名兼容的EndMethod
,因此我们可以毫无问题地将这个委托异步调用转换为任务。
在第二个示例中,我们做了几乎相同的事情,但使用了不同的FromAsync
方法重载,它不允许指定在异步委托调用完成后将执行的回调。我们可以用延续来替换这个,但如果回调很重要,我们可以使用第一个示例。
最后一个示例展示了一个小技巧。这次,IncompatibleAsynchronousTask
委托的EndMethod
使用了out
参数,并且与任何FromAsync
方法重载都不兼容。然而,很容易将EndMethod
调用包装成适用于任务工厂的 lambda 表达式。
为了查看底层任务的情况,我们在等待异步操作结果时打印其状态。我们看到第一个任务的状态是WaitingForActivation
,这意味着任务实际上还没有被 TPL 基础架构启动。
将 EAP 模式转换为任务
本教程将描述如何将基于事件的异步操作转换为任务。在本教程中,您将找到一个适用于.NET Framework 类库中的每个基于事件的异步 API 的可靠模式。
准备工作
要开始本教程,您需要 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在BookSamples\Chapter4\Recipe5
中找到。
如何做...
要将 EAP 模式转换为任务,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static int TaskMethod(string name, int seconds){
Console.WriteLine("Task {0} is running on a thread id{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
return 42 * seconds;
}
- 在
Main
方法内添加以下代码片段:
var tcs = new TaskCompletionSource<int>();
var worker = new BackgroundWorker();
worker.DoWork += (sender, eventArgs) =>
{
eventArgs.Result = TaskMethod("Background worker", 5);
};
worker.RunWorkerCompleted += (sender, eventArgs) =>{
if (eventArgs.Error != null) {
tcs.SetException(eventArgs.Error);
}
else if (eventArgs.Cancelled) {
tcs.SetCanceled();
}
else {
tcs.SetResult((int)eventArgs.Result);
}
};
worker.RunWorkerAsync();
int result = tcs.Task.Result;
Console.WriteLine("Result is: {0}", result);
- 运行程序。
工作原理...
这是一个非常简单而优雅的将 EAP 模式转换为任务的例子。关键点是使用TaskCompletionSource<T>
类型,其中T
是异步操作的结果类型。
同样重要的是不要忘记将tcs.SetResult
方法调用包装在try
-catch
块中,以确保错误信息始终设置到任务完成源对象中。也可以使用TrySetResult
方法代替SetResult
,以确保结果已成功设置。
实现取消选项
本教程是关于为基于任务的异步操作实现取消过程。我们将学习如何正确使用取消令牌来处理任务,以及如何在任务实际运行之前找出任务是否已取消。
准备工作
要开始本教程,您需要 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在BookSamples\Chapter4\Recipe6
中找到。
如何做...
要为基于任务的异步操作实现取消选项,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
private static int TaskMethod(string name, int seconds,CancellationToken token){
Console.WriteLine("Task {0} is running on a thread id{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
for (int i = 0; i< seconds; i ++) {
Thread.Sleep(TimeSpan.FromSeconds(1));
if (token.IsCancellationRequested)
return -1;
}
return 42*seconds;
}
- 在
Main
方法内添加以下代码片段:
var cts = new CancellationTokenSource();
var longTask = new Task<int>(() =>TaskMethod("Task 1", 10,cts.Token), cts.Token);
Console.WriteLine(longTask.Status);
cts.Cancel();
Console.WriteLine(longTask.Status);
Console.WriteLine("First task has been cancelled beforeexecution");
cts = new CancellationTokenSource();
longTask = new Task<int>(() =>TaskMethod("Task 2", 10,cts.Token), cts.Token);
longTask.Start();
for (int i = 0; i< 5; i++ ){
Thread.Sleep(TimeSpan.FromSeconds(0.5));
Console.WriteLine(longTask.Status);
}
cts.Cancel();
for (int i = 0; i< 5; i++){
Thread.Sleep(TimeSpan.FromSeconds(0.5));
Console.WriteLine(longTask.Status);
}
Console.WriteLine("A task has been completed with result{0}.", longTask.Result);
- 运行程序。
工作原理...
这是另一个非常简单的例子,说明如何为 TPL 任务实现取消选项,因为你已经熟悉我们在第三章中讨论的取消标记概念,使用线程池。
首先,让我们仔细看看 longTask
创建代码。我们将一次性传递一个取消标记给底层任务,然后第二次传递给任务构造函数。为什么我们需要两次提供这个标记?
答案是,如果我们在任务实际开始之前取消了任务,它的 TPL 基础结构负责处理取消,因为我们的代码根本不会执行。我们知道第一个任务被取消了,通过获取它的状态。如果我们尝试在这个任务上调用 Start
方法,我们将得到 InvalidOperationException
。
然后,我们从我们自己的代码中处理取消过程。这意味着我们现在完全负责取消过程,而在我们取消任务后,它的状态仍然是 RanToCompletion
,因为从 TPL 的角度来看,任务正常完成了它的工作。在每种情况下理解责任差异非常重要。
处理任务中的异常
这个步骤描述了在异步任务中处理异常的非常重要的主题。我们将讨论从任务中抛出的异常发生的不同方面以及如何获取它们的信息。
准备就绪
要按照这个步骤,你需要 Visual Studio 2012。没有其他先决条件。这个步骤的源代码可以在 BookSamples\Chapter4\Recipe7
中找到。
如何做...
要处理任务中的异常,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序 项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static int TaskMethod(string name, int seconds){
Console.WriteLine("Task {0} is running on a thread id{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
throw new Exception("Boom!");
return 42 * seconds;
}
- 在
Main
方法中添加以下代码片段:
Task<int> task;
try{
task = Task.Run(() =>TaskMethod("Task 1", 2));
int result = task.Result;
Console.WriteLine("Result: {0}", result);
}
catch (Exception ex){
Console.WriteLine("Exception caught: {0}", ex);
}
Console.WriteLine("----------------------------------------------");
Console.WriteLine();
try{
task = Task.Run(() =>TaskMethod("Task 2", 2));
int result = task.GetAwaiter().GetResult();
Console.WriteLine("Result: {0}", result);
}
catch (Exception ex){
Console.WriteLine("Exception caught: {0}", ex);
}
Console.WriteLine("----------------------------------------------");
Console.WriteLine();
var t1 = new Task<int>(() =>TaskMethod("Task 3", 3));
var t2 = new Task<int>(() =>TaskMethod("Task 4", 2));
var complexTask = Task.WhenAll(t1, t2);
var exceptionHandler = complexTask.ContinueWith(t =>Console.WriteLine("Exception caught: {0}", t.Exception),TaskContinuationOptions.OnlyOnFaulted);
t1.Start();
t2.Start();
Thread.Sleep(TimeSpan.FromSeconds(5));
- 运行程序。
它是如何工作的...
程序启动时,我们创建一个任务,并尝试同步获取任务结果。Result
属性的 Get
部分使当前线程等待任务完成,并将异常传播到当前线程。在这种情况下,我们很容易在 catch 块中捕获异常,但这个异常是一个名为 AggregateException
的包装异常。在这种情况下,它只包含一个异常,因为只有一个任务抛出了这个异常,可以通过访问 InnerException
属性来获取底层异常。
第二个例子大部分相同,但是为了访问任务结果,我们使用 GetAwaiter
和 GetResult
方法。在这种情况下,我们没有包装异常,因为它被 TPL 基础结构解包了。我们一次性获得原始异常,如果只有一个底层任务,这是非常舒适的。
最后一个例子展示了我们有两个任务抛出异常的情况。为了处理异常,我们现在使用一个继续,只有在前置任务以异常结束时才执行。通过为继续提供 TaskContinuationOptions.OnlyOnFaulted
选项来实现这种行为。结果,我们打印出 AggregateException
,并且其中包含来自两个任务的两个内部异常。
还有更多...
由于任务可能以非常不同的方式连接,因此生成的 AggregateException
异常可能包含其他聚合异常以及通常的异常。这些内部聚合异常本身可能包含其中的其他聚合异常。
为了摆脱这些包装器,我们应该使用根聚合异常的 Flatten
方法。它将返回层次结构中每个子聚合异常的所有内部异常的集合。
并行运行任务
这个示例展示了如何处理同时运行的许多异步任务。我们将学习如何在所有任务完成或任何正在运行的任务必须完成它们的工作时有效地得到通知。
准备工作
要开始这个示例,你需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter4\Recipe8
中找到。
如何做...
要并行运行任务,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static int TaskMethod(string name, int seconds){
Console.WriteLine("Task {0} is running on a thread id{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
return 42 * seconds;
}
- 在
Main
方法内部添加以下代码片段:
var firstTask = new Task<int>(() =>TaskMethod("First Task",3));
var secondTask = new Task<int>(() =>TaskMethod("SecondTask", 2));
var whenAllTask = Task.WhenAll(firstTask, secondTask);
whenAllTask.ContinueWith(t =>
Console.WriteLine("The first answer is {0}, the second is{1}", t.Result[0], t.Result[1]),TaskContinuationOptions.OnlyOnRanToCompletion);
firstTask.Start();
secondTask.Start();
Thread.Sleep(TimeSpan.FromSeconds(4));
var tasks = new List<Task<int>>();
for (int i = 1; i< 4; i++)
{
int counter = i;
var task = new Task<int>(() =>TaskMethod(string.Format("Task {0}", counter), counter));
tasks.Add(task);
task.Start();
}
while (tasks.Count> 0){
var completedTask = Task.WhenAny(tasks).Result;
tasks.Remove(completedTask);
Console.WriteLine("A task has been completed with result{0}.", completedTask.Result);
}
Thread.Sleep(TimeSpan.FromSeconds(1));
- 运行程序。
工作原理...
程序启动时,我们创建两个任务,然后借助Task.WhenAll
方法创建一个第三个任务,该任务将在所有任务完成后完成。结果任务为我们提供了一个答案数组,其中第一个元素保存第一个任务的结果,第二个元素保存第二个结果,依此类推。
然后,我们创建另一个任务列表,并使用Task.WhenAny
方法等待其中任何一个任务完成。在我们有一个完成的任务后,我们将其从列表中移除,并继续等待其他任务完成,直到列表为空。这种方法对于获取任务的完成进度或在运行任务时使用超时非常有用。例如,我们等待一些任务,其中一个任务正在计算超时。如果这个任务首先完成,我们就取消那些尚未完成的任务。
使用 TaskScheduler 调整任务执行
这个示例描述了处理任务的另一个非常重要的方面,即从异步代码中正确处理 UI 的方法。我们将学习任务调度程序是什么,为什么它如此重要,它如何损害我们的应用程序,以及如何使用它来避免错误。
准备工作
要完成这个示例,你需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter4\Recipe9
中找到。
如何做...
通过使用TaskScheduler
调整任务执行,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# WPF 应用程序项目。这一次,我们将需要一个带有消息循环的 UI 线程,这在控制台应用程序中是不可用的。
-
在
MainWindow.xaml
文件中,在一个网格元素内添加以下标记(即在<Grid>
和</Grid>
标记之间):
<TextBlock Name="ContentTextBlock"
HorizontalAlignment="Left"
Margin="44,134,0,0"
VerticalAlignment="Top"
Width="425"
Height="40"/>
<Button Content="Sync"
HorizontalAlignment="Left"
Margin="45,190,0,0"
VerticalAlignment="Top"
Width="75"
Click="ButtonSync_Click"/>
<Button Content="Async"
HorizontalAlignment="Left"
Margin="165,190,0,0"
VerticalAlignment="Top"
Width="75"
Click="ButtonAsync_Click"/>
<Button Content="Async OK"
HorizontalAlignment="Left"
Margin="285,190,0,0"
VerticalAlignment="Top"
Width="75"
Click="ButtonAsyncOK_Click"/>
- 在
MainWindow.xaml.cs
文件中,使用以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
- 在
MainWindow
构造函数下面添加以下代码片段:
void ButtonSync_Click(object sender, RoutedEventArgs e){
ContentTextBlock.Text = string.Empty;
try {
//string result = TaskMethod(TaskScheduler.//FromCurrentSynchronizationContext()).Result;
string result = TaskMethod().Result;
ContentTextBlock.Text = result;
}
catch (Exception ex) {
ContentTextBlock.Text = ex.InnerException.Message;
}
}
void ButtonAsync_Click(object sender, RoutedEventArgs e) {
ContentTextBlock.Text = string.Empty;
Mouse.OverrideCursor = Cursors.Wait;
Task<string> task = TaskMethod();
task.ContinueWith(t => {
ContentTextBlock.Text = t.Exception.InnerException.Message;
Mouse.OverrideCursor = null;
},
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext());
}
void ButtonAsyncOK_Click(object sender, RoutedEventArgs e){
ContentTextBlock.Text = string.Empty;
Mouse.OverrideCursor = Cursors.Wait;
Task<string> task = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext());
task.ContinueWith(t =>Mouse.OverrideCursor = null,
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
Task<string> TaskMethod() {
return TaskMethod(TaskScheduler.Default);
}
Task<string> TaskMethod(TaskScheduler scheduler) {
Task delay = Task.Delay(TimeSpan.FromSeconds(5));
return delay.ContinueWith(t => {
string str = string.Format("Task is running on a threadid {0}. Is thread pool thread: {1}",Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
ContentTextBlock.Text = str;
return str;
}, scheduler);
}
- 运行程序。
工作原理...
在这里,我们遇到了许多新的东西。首先,我们创建了一个 WPF 应用程序,而不是控制台应用程序。这是必要的,因为我们需要一个用户界面线程和消息循环来演示异步运行任务的不同选项。
有一个非常重要的抽象叫做TaskScheduler
。这个组件实际上负责任务的执行方式。默认的任务调度程序将任务放在线程池工作线程上。这是最常见的情况,也不奇怪它是 TPL 中的默认选项。我们还知道如何同步运行任务,以及如何将它们附加到父任务以一起运行。现在让我们看看我们可以用任务做什么。
程序启动时,我们创建一个带有三个按钮的窗口。第一个按钮调用同步任务执行。代码放在ButtonSync_Click
方法中。当任务运行时,即使我们无法移动应用程序窗口。用户界面在任务运行时完全冻结,直到任务完成之前,用户界面线程无法响应任何消息循环。这是 GUI Windows 应用程序的一个常见的不良实践,我们需要找到一种解决这个问题的方法。
第二个问题是,我们试图从另一个线程访问 UI 控件。图形用户界面控件从未设计为从多个线程中使用,并且为了避免可能的错误,不允许您从创建它的线程之外的线程访问这些组件。当我们尝试这样做时,我们会收到异常,并且异常消息将在 5 秒钟后打印在主窗口中。
为了解决第一个问题,我们尝试异步运行任务。这就是第二个按钮的作用;其中的代码放在ButtonAsync_Click
方法中。如果在调试器下运行任务,您将看到它被放置在线程池中,最后,我们将得到相同的异常。然而,用户界面在任务运行时始终保持响应。这是一件好事,但我们需要摆脱异常。
我们已经做到了!为了输出错误消息,使用了TaskScheduler.FromCurrentSynchronizationContext
选项提供了一个继续。如果不这样做,我们将看不到错误消息,因为我们会得到与任务内部发生的相同异常。此选项指示 TPL 基础结构将代码放在 UI 线程的继续中,并借助 UI 线程消息循环异步运行它。这解决了从另一个线程访问 UI 控件的问题,但仍然保持了我们的 UI 响应性。
要检查这是否属实,我们按下最后一个按钮,运行ButtonAsyncOK_Click
方法中的代码。唯一不同的是,我们为我们的任务提供了 UI 线程任务调度程序。任务完成后,您将看到它以异步方式在 UI 线程上运行。UI 保持响应,并且即使等待光标处于活动状态,也可以按下另一个按钮。
然而,对于在 UI 线程上运行任务有一些技巧。如果我们回到同步任务代码,并取消注释使用 UI 线程任务调度程序获取结果的行,我们将永远得不到任何结果。这是一个经典的死锁情况:我们正在将操作调度到 UI 线程的队列中,而 UI 线程等待此操作完成,但当它等待时,它无法运行操作,这将永远不会结束(甚至不会开始)。如果在任务上调用Wait
方法,也会发生这种情况。为了避免死锁,永远不要在计划为 UI 线程的任务上使用同步操作;只使用ContinueWith
,或者来自 C# 5.0 的async
/await
。
第五章:使用 C# 5.0
在本章中,我们将研究 C# 5.0 编程语言中的本机异步编程支持。您将了解以下内容:
-
使用 await 运算符获取异步任务结果
-
在 lambda 表达式中使用 await 运算符
-
使用 await 运算符与随后的异步任务
-
使用 await 运算符执行并行异步任务
-
处理异步操作中的异常
-
避免使用捕获的同步上下文
-
解决异步 void 方法的问题
-
设计自定义可等待类型
-
使用动态类型与 await
介绍
到目前为止,我们了解了来自 Microsoft 的最新异步编程基础设施——任务并行库。它允许我们以模块化的方式设计程序,将不同的异步操作组合在一起。
不幸的是,当阅读这样的程序时,仍然很难理解实际的程序流程。在一个大型程序中,将会有许多任务和依赖于彼此的延续,运行其他延续的延续,用于异常处理的延续,它们都聚集在程序代码中的非常不同的地方。因此,理解哪个操作先进行,接下来发生什么的顺序成为一个非常具有挑战性的问题。
另一个需要注意的问题是要查看是否将适当的同步上下文传播到可能触及用户界面控件的每个异步任务。只有从 UI 线程才允许使用这些控件;否则,我们将得到一个多线程访问异常。
谈到异常,我们还必须使用单独的延续任务来处理发生在前置异步操作或操作中的错误。这反过来导致了复杂的错误处理代码,分散在代码的不同部分,彼此之间没有逻辑关联。
为了解决这些问题,C# 5.0 的作者引入了称为异步函数的新语言增强功能。它们确实使异步编程变得简单,但同时,它是 TPL 的高级抽象。正如我们在第四章中提到的,使用任务并行库,抽象隐藏了重要的实现细节,并使异步编程更加容易,但却剥夺了程序员的许多重要内容。了解异步函数背后的概念对于创建健壮和可扩展的应用程序非常重要。
要创建一个异步函数,首先要用async
关键字标记一个方法。在没有这样做之前,不可能拥有带有 async 属性或事件访问器方法和构造函数。代码将如下所示:
async Task<string> GetStringAsync()
{
await Task.Delay(TimeSpan.FromSeconds(2));
return "Hello, World!";
}
另一个重要的事实是,异步函数必须返回Task
或Task<T>
类型。可以有async void
方法,但最好使用async Task
方法。只有在应用程序中使用顶层 UI 控件事件处理程序时,才能使用async void
函数。
在标记有async
关键字的方法内部,可以使用await
运算符。该运算符与 TPL 中的任务一起工作,并获取任务内部异步操作的结果。详细内容将在本章后面介绍。您不能在async
方法之外使用await
运算符;这将导致编译错误。此外,异步函数应该至少在其代码中有一个await
运算符。但这只会导致编译警告,而不是错误。
重要的是要注意,在await
调用的行之后,此方法立即返回。在同步执行的情况下,执行线程将被阻塞 2 秒,然后返回结果。在这里,我们在返回一个工作线程到线程池的同时异步等待,立即在执行await
操作符后返回一个工作线程。2 秒后,我们再次从线程池中获取工作线程,并在其上运行其余的异步方法。这使我们能够在这 2 秒内重复使用这个工作线程来做一些其他工作,这对应用程序的可伸缩性非常重要。通过异步函数的帮助,我们有一个线性的程序控制流,但它仍然是异步的。这既非常舒适又非常令人困惑。本章的食谱将帮助您学习异步函数的每个重要方面。
注意
根据我的经验,如果程序中有两个连续的await
操作符,人们普遍存在一个误解。许多人认为,如果我们在一个异步操作之后使用await
函数,它们会并行运行。然而,它们实际上是顺序运行的;第二个操作只有在第一个操作完成后才开始。记住这一点非常重要,在本章的后面,我们将详细讨论这个话题。
在 C# 5.0 中使用async
和await
存在一些限制。例如,不可能将控制台应用程序的Main
方法标记为async
;您不能在catch
、finally
、lock
或unsafe
块中使用await
操作符。异步函数上不允许有ref
和out
参数。还有更多微妙之处,但这些是主要的要点。
异步函数在幕后由 C#编译器转换为复杂的程序构造。我故意不会详细描述这一点;生成的代码与另一个 C#构造,称为迭代器,非常相似,并且实现为一种状态机。由于许多开发人员几乎在每个方法上都开始使用async
修饰符,我想强调的是,如果一个方法不打算以异步或并行方式使用,那么将方法标记为async
是没有意义的。调用async
方法会带来显著的性能损失,通常方法调用将比标记为async
关键字的相同方法快 40 到 50 倍。请注意这一点。
在本章中,我们将学习如何使用 C# 5.0 的async
和await
关键字来处理异步操作。我们将讨论如何顺序和并行等待异步操作。我们将讨论如何在 lambda 表达式中使用await
,如何处理异常,以及在使用async void
方法时如何避免陷阱。最后,我们将深入探讨同步上下文传播,并学习如何创建自己的可等待对象,而不是使用任务。
使用await
操作符获取异步任务结果
这个食谱介绍了使用异步函数的基本场景。我们将比较如何使用 TPL 和await
操作符获取异步操作结果。
准备就绪
要按照这个食谱,您需要 Visual Studio 2012。没有其他先决条件。此食谱的源代码可以在BookSamples\Chapter5\Recipe1
中找到。
如何做...
使用await
操作符获取异步任务结果的步骤如下:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static Task AsynchronyWithTPL()
{
Task<string> t = GetInfoAsync("Task 1");
Task t2 = t.ContinueWith(task => Console.WriteLine(t.Result), TaskContinuationOptions.NotOnFaulted);
Task t3 = t.ContinueWith(task => Console.WriteLine(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted);
return Task.WhenAny(t2, t3);
}
async static Task AsynchronyWithAwait()
{
try
{
string result = await GetInfoAsync("Task 2");
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
async static Task<string> GetInfoAsync(string name)
{
await Task.Delay(TimeSpan.FromSeconds(2));
//throw new Exception("Boom!");
return string.Format("Task {0} is running on a thread id{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
}
- 在
Main
方法中添加以下代码片段:
Task t = AsynchronyWithTPL();
t.Wait();
t = AsynchronyWithAwait();
t.Wait();
- 运行程序。
工作原理...
当程序运行时,我们运行两个异步操作。其中一个是标准的 TPL 驱动代码,另一个使用新的async
和await
C#特性。AsynchronyWithTPL
方法启动一个运行 2 秒钟的任务,然后返回一个包含有关工作线程信息的字符串。然后,我们定义一个继续打印异步操作结果的操作,另一个用于在发生错误时打印异常详细信息。最后,我们返回表示一个继续任务的任务,并在Main
方法中等待其完成。
在AsynchronyWithAwait
方法中,我们通过使用await
与任务实现了相同的结果。就好像我们只是编写了普通的同步代码-我们从任务中获取结果,打印结果,并在任务完成时捕获异常。关键区别在于我们实际上有一个异步程序。在使用await
后立即,C#创建了一个任务,该任务具有在await
运算符之后的所有剩余代码的继续任务,并处理异常传播。然后,我们将此任务返回给Main
方法,并等待其完成。
注意
请注意,根据底层异步操作的性质和当前的同步上下文,执行异步代码的确切方式可能有所不同。我们将在本章后面解释这一点。
因此,我们可以看到程序的第一部分和第二部分在概念上是等价的,但在第二部分中,C#编译器隐式地处理异步代码。实际上,它甚至比第一部分更复杂,我们将在本章的接下来的几个食谱中详细介绍。
请记住,在诸如 Windows GUI 或 ASP.NET 之类的环境中,不建议使用Task.Wait
和Task.Result
方法。如果程序员对代码的实际情况不是 100%了解,这可能会导致死锁。这在第四章的使用任务并行库中的使用 TaskScheduler 调整任务执行食谱中有所说明,当我们在 WPF 应用程序中使用Task.Result
时。
要测试异常处理的工作原理,只需取消注释GetInfoAsync
方法中的throw new Exception
行。
在 lambda 表达式中使用 await 运算符
这个食谱将展示如何在 lambda 表达式中使用await
。我们将编写一个使用await
的匿名方法,并异步地获得方法执行的结果。
准备工作
要按照这个食谱进行操作,您需要 Visual Studio 2012。没有其他先决条件。此食谱的源代码可以在BookSamples\Chapter5\Recipe2
中找到。
如何做...
要编写一个使用await
的匿名方法,并通过在 lambda 表达式中使用await
运算符异步地获得方法执行的结果,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
async static Task AsynchronousProcessing()
{
Func<string, Task<string>> asyncLambda = async name => {
await Task.Delay(TimeSpan.FromSeconds(2));
return string.Format("Task {0} is running on a threadid {1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
};
string result = await asyncLambda("async lambda");
Console.WriteLine(result);
}
- 在
Main
方法中添加以下代码片段:
Task t = AsynchronousProcessing();
t.Wait();
- 运行程序。
工作原理...
首先,我们将异步函数移到AsynchronousProcessing
方法中,因为我们不能在Main
方法中使用async
。然后,我们使用async
关键字描述一个 lambda 表达式。由于任何 lambda 表达式的类型不能从 lambda 本身推断出来,我们必须明确地向 C#编译器指定其类型。在我们的情况下,类型意味着我们的 lambda 接受一个字符串参数,并返回一个Task<string>
对象。
然后,我们定义 lambda 表达式的主体。一个异常是,该方法被定义为返回一个Task<string>
对象,但实际上我们返回一个字符串,并且没有编译错误!C#编译器会自动生成一个任务并为我们返回它。
最后一步是等待异步 lambda 表达式的执行并打印出结果。
使用 await 操作符进行连续异步任务的执行
这个步骤将展示当代码中有几个连续的await
方法时,程序流程是如何的。我们将学习如何阅读带有await
方法的代码,并理解为什么await
调用是一个异步操作。
准备工作
要按照这个步骤,你需要 Visual Studio 2012。没有其他先决条件。这个步骤的源代码可以在BookSamples\Chapter5\Recipe3
找到。
如何做...
理解在连续的await
方法存在的情况下程序流程,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static Task AsynchronyWithTPL()
{
var containerTask = new Task(() => {
Task<string> t = GetInfoAsync("TPL 1");
t.ContinueWith(task => {
Console.WriteLine(t.Result);
Task<string> t2 = GetInfoAsync("TPL 2");
t2.ContinueWith(innerTask =>Console.WriteLine(innerTask.Result),TaskContinuationOptions.NotOnFaulted |TaskContinuationOptions.AttachedToParent);
t2.ContinueWith(innerTask =>Console.WriteLine(innerTask.Exception.InnerException),TaskContinuationOptions.OnlyOnFaulted |TaskContinuationOptions.AttachedToParent);
},
TaskContinuationOptions.NotOnFaulted |TaskContinuationOptions.AttachedToParent);
t.ContinueWith(task =>Console.WriteLine(t.Exception.InnerException),TaskContinuationOptions.OnlyOnFaulted |TaskContinuationOptions.AttachedToParent);
});
containerTask.Start();
return containerTask;
}
async static Task AsynchronyWithAwait()
{
try
{
string result = await GetInfoAsync("Async 1");
Console.WriteLine(result);
result = await GetInfoAsync("Async 2");
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
async static Task<string> GetInfoAsync(string name)
{
Console.WriteLine("Task {0} started!", name);
await Task.Delay(TimeSpan.FromSeconds(2));
if(name == "TPL 2")
throw new Exception("Boom!");
return string.Format("Task {0} is running on a thread id{1}. Is thread pool thread: {2}",name, Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
}
- 在
Main
方法内添加以下代码片段:
Task t = AsynchronyWithTPL();
t.Wait();
t = AsynchronyWithAwait();
t.Wait();
- 运行程序。
工作原理...
当程序运行时,我们运行两个异步操作,就像在第一个步骤中一样。然而,这一次我们将从AsynchronyWithAwait
方法开始。它看起来仍然像通常的同步代码;唯一的区别是两个await
语句。最重要的一点是,代码仍然是顺序的,Async 2
任务只有在前一个任务完成后才会开始。当我们阅读代码时,程序流程非常清晰:我们看到什么先运行,然后是什么之后。那么,这个程序是如何异步的呢?嗯,首先,它并不总是异步的。如果一个任务在我们使用await
时已经完成,我们将同步地得到它的结果。否则,当我们在代码中看到await
语句时,通常的做法是注意到此时方法将立即返回,剩下的代码将在一个继续任务中运行。由于我们不阻塞等待操作的结果,这是一个异步调用。我们可以在Main
方法中调用t.Wait
之外的任何其他任务,而AsynchronyWithAwait
方法中的代码正在执行。然而,主线程必须等待直到所有异步操作完成,否则它们将在后台线程上运行时被停止。
AsynchronyWithTPL
方法模拟了与AsynchronyWithAwait
方法相同的程序流程。我们需要一个容器任务来一起处理所有依赖任务。然后,我们启动主任务,并为其添加一组继续任务。当任务完成时,我们打印出结果;然后,我们启动另一个任务,该任务在第二个任务完成后继续工作。为了测试异常处理,我们故意在运行第二个任务时抛出异常,并打印出其信息。这一系列的继续任务创建了与第一种方法相同的程序流程,当我们将其与带有await
方法的代码进行比较时,我们可以看到它更容易阅读和理解。唯一的诀窍是要记住,异步并不总是意味着并行执行。
使用 await 操作符执行并行异步任务执行
在这个步骤中,我们将学习如何使用await
来并行运行异步操作,而不是通常的顺序执行。
准备工作
要按照这个步骤,你需要 Visual Studio 2012。没有其他先决条件。这个步骤的源代码可以在BookSamples\Chapter5\Recipe4
找到。
如何做...
要理解使用await
操作符进行并行异步任务执行,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码:
async static Task AsynchronousProcessing()
{
Task<string> t1 = GetInfoAsync("Task 1", 3);
Task<string> t2 = GetInfoAsync("Task 2", 5);
string[] results = await Task.WhenAll(t1, t2);
foreach (string result in results)
{
Console.WriteLine(result);
}
}
async static Task<string> GetInfoAsync(string name, int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
/*await Task.Run(() =>Thread.Sleep(TimeSpan.FromSeconds(seconds)));*/
return string.Format("Task {0} is running on a thread id{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
}
- 在
Main
方法内添加以下代码片段:
Task t = AsynchronousProcessing();
t.Wait();
- 运行程序。
工作原理...
在这里,我们定义了两个分别运行 3 秒和 5 秒的异步任务。然后,我们使用Task.WhenAll
辅助方法创建另一个任务,只有当所有底层任务完成时才会完成。然后,我们等待此组合任务的结果。5 秒后,我们得到了所有结果,这意味着任务是同时运行的。
然而,有一个有趣的观察。当您运行程序时,您可能会注意到两个任务很可能由线程池中的同一个工作线程提供服务。当我们并行运行任务时,这是如何可能的?为了使事情更有趣,让我们注释掉GetIntroAsync
方法中的await Task.Delay
行,并取消注释await Task.Run
行,然后运行程序。
我们将看到在这种情况下,两个任务将由不同的工作线程提供服务。不同之处在于Task.Delay
在内部使用了一个计时器,处理过程如下:我们从线程池中获取工作线程,它等待Task.Delay
方法返回结果。然后,Task.Delay
方法启动计时器,并指定在计时器计算Task.Delay
方法指定的秒数时将调用的代码。然后我们立即将工作线程返回到线程池。当计时器事件运行时,我们再次从线程池中获取任何可用的工作线程(可能是我们首先使用的相同线程),并在其上运行计时器提供的代码。
当我们使用Task.Run
方法时,我们从线程池中获取一个工作线程,并使其阻塞一段时间,提供给Thread.Sleep
方法。然后,我们获取第二个工作线程并阻塞它。在这种情况下,我们消耗了两个工作线程,它们完全没有做任何事情,无法执行任何其他任务。
我们将在第九章中详细讨论第一种情况,使用异步 I/O,在那里我们将讨论一大堆与数据输入和输出一起工作的异步操作。在可能的情况下始终使用第一种方法是创建可扩展服务器应用程序的关键。
在异步操作中处理异常
本示例将描述如何在 C#中使用异步函数处理异常。我们将学习如何处理使用await
进行多个并行异步操作时的聚合异常。
准备工作
要执行此示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter5\Recipe5
中找到。
如何做到这一点...
要了解异步操作中的异常处理,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
async static Task AsynchronousProcessing()
{
Console.WriteLine("1\. Single exception");
try
{
string result = await GetInfoAsync("Task 1", 2);
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine("Exception details: {0}", ex);
}
Console.WriteLine();
Console.WriteLine("2\. Multiple exceptions");
Task<string> t1 = GetInfoAsync("Task 1", 3);
Task<string> t2 = GetInfoAsync("Task 2", 2);
try
{
string[] results = await Task.WhenAll(t1, t2);
Console.WriteLine(results.Length);
}
catch (Exception ex)
{
Console.WriteLine("Exception details: {0}", ex);
}
Console.WriteLine();
Console.WriteLine("2\. Multiple exceptions with AggregateException");
t1 = GetInfoAsync("Task 1", 3);
t2 = GetInfoAsync("Task 2", 2);
Task<string[]> t3 = Task.WhenAll(t1, t2);
try
{
string[] results = await t3;
Console.WriteLine(results.Length);
}
catch
{
var ae = t3.Exception.Flatten();
var exceptions = ae.InnerExceptions;
Console.WriteLine("Exceptions caught: {0}", exceptions.Count);
foreach (var e in exceptions)
{
Console.WriteLine("Exception details: {0}", e);
Console.WriteLine();
}
}
}
async static Task<string> GetInfoAsync(string name, int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
throw new Exception(string.Format("Boom from {0}!", name));
}
- 在
Main
方法中添加以下代码片段:
Task t = AsynchronousProcessing();
t.Wait();
- 运行程序。
它是如何工作的...
我们运行三个场景来说明在 C#中使用async
和await
处理错误的最常见情况。第一种情况非常简单,几乎与通常的同步代码相同。我们只是使用try
/catch
语句并获取异常的详细信息。
常见的错误是在等待多个异步操作时使用相同的方法。如果我们像以前一样使用catch
块,我们将只从底层的AggregateException
对象中得到第一个异常。
为了收集所有信息,我们必须使用等待任务的Exception
属性。在第三种情况下,我们展平AggregateException
层次结构,然后使用AggregateException
的Flatten
方法解开其中的所有异常。
避免使用捕获的同步上下文
本教程讨论了使用await
获取异步操作结果时同步上下文行为的细节。我们将学习何时以及如何关闭同步上下文流。
准备就绪
要按照本教程进行,您需要 Visual Studio 2012。没有其他先决条件。本教程的源代码可以在BookSamples\Chapter5\Recipe6
中找到。
如何做...
要了解使用await
时同步上下文行为的细节,并学习何时以及如何关闭同步上下文流,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# Console Application项目。
-
添加对 Windows Presentation Foundation Library 的引用。
-
在项目中右键单击References文件夹,然后选择Add reference…菜单选项。
-
添加对以下库的引用:PresentationCore,PresentationFramework,System.Xaml和Windows.Base。您可以使用引用管理器对话框中的搜索功能,如下所示:
- 在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
- 在
Main
方法下面添加以下代码片段:
private static Label _label;
async static void Click(object sender, EventArgs e)
{
_label.Content = new TextBlock {Text = "Calculating..."};
TimeSpan resultWithContext = await Test();
TimeSpan resultNoContext = await TestNoContext();
/*TimeSpan resultNoContext = awaitTestNoContext().ConfigureAwait(false);*/
var sb = new StringBuilder();
sb.AppendLine(string.Format("With the context: {0}",resultWithContext));
sb.AppendLine(string.Format("Without the context: {0}",resultNoContext));
sb.AppendLine(string.Format("Ratio: {0:0.00}",resultWithContext.TotalMilliseconds/resultNoContext.TotalMilliseconds));
_label.Content = new TextBlock {Text = sb.ToString()};
}
async static Task<TimeSpan> Test()
{
const int iterationsNumber = 100000;
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < iterationsNumber; i++)
{
var t = Task.Run(() => { });
await t;
}
sw.Stop();
return sw.Elapsed;
}
async static Task<TimeSpan> TestNoContext()
{
const int iterationsNumber = 100000;
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < iterationsNumber; i++)
{
var t = Task.Run(() => { });
await t.ConfigureAwait(continueOnCapturedContext: false);
}
sw.Stop();
return sw.Elapsed;
}
- 用以下代码片段替换
Main
方法:
[STAThread]
static void Main(string[] args)
{
var app = new Application();
var win = new Window();
var panel = new StackPanel();
var button = new Button();
_label = new Label();
_label.FontSize = 32;
_label.Height = 200;
button.Height = 100;
button.FontSize = 32;
button.Content = new TextBlock {Text = "Start asynchronous operations"};
button.Click += Click;
panel.Children.Add(_label);
panel.Children.Add(button);
win.Content = panel;
app.Run(win);
Console.ReadLine();
}
- 运行程序。
工作原理...
在这个例子中,我们将研究异步函数默认行为的最重要方面之一。我们已经从第四章使用任务并行库中了解了任务调度程序和同步上下文。默认情况下,await
操作符会尝试捕获同步上下文,并在其上执行后续代码。正如我们已经知道的那样,这有助于我们通过使用用户界面控件编写异步代码。此外,使用await
时不会发生死锁情况,因为我们在等待结果时不会阻塞 UI 线程,就像在上一章中描述的那样。
这是合理的,但让我们看看可能发生的情况。在这个例子中,我们通过编程方式创建了一个 Windows Presentation Foundation 应用程序,并订阅了它的按钮点击事件。单击按钮时,我们运行两个异步操作。其中一个使用常规的await
操作符,而另一个使用ConfigureAwait
方法,并将false
作为参数值。它明确指示我们不应该使用捕获的同步上下文来在其上运行继续代码。在每个操作中,我们测量它们完成所需的时间,然后在主屏幕上显示相应的时间和比率。
结果是,我们看到常规的await
操作符需要更长的时间才能完成。这是因为我们在 UI 线程上发布了十万个继续任务,它使用其消息循环来异步处理这些任务。在这种情况下,我们不需要此代码在 UI 线程上运行,因为我们不从异步操作中访问 UI 组件;使用ConfigureAwait
和false
将是一个更有效的解决方案。
还有一件值得注意的事情。尝试只点击按钮运行程序并等待结果。现在再做同样的事情,但这次在点击按钮的同时尝试随机拖动应用程序窗口的一侧。您会注意到捕获的同步上下文中的代码变得更慢!这个有趣的副作用完美地说明了异步编程是多么危险。很容易遇到这样的情况,如果您以前从未经历过这样的行为,几乎不可能进行调试。
公平起见,让我们看看相反的情况。在前面的代码片段中,在Click
方法内部取消注释已注释的行,并注释其前面的行。运行应用程序时,我们将收到一个多线程控制访问异常,因为设置Label
控件文本的代码不会发布在捕获的上下文上,而是在一个线程池工作线程上执行。
解决async void
方法的问题
本示例描述了为什么使用async void
方法非常危险。我们将学习在什么情况下可以使用此方法,以及在可能的情况下应该使用什么。
准备工作
要执行此示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter5\Recipe7
中找到。
如何做...
要学习如何使用async void
方法,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
async static Task AsyncTaskWithErrors()
{
string result = await GetInfoAsync("AsyncTaskException",2);
Console.WriteLine(result);
}
async static void AsyncVoidWithErrors()
{
string result = await GetInfoAsync("AsyncVoidException",2);
Console.WriteLine(result);
}
async static Task AsyncTask()
{
string result = await GetInfoAsync("AsyncTask", 2);
Console.WriteLine(result);
}
private static async void AsyncVoid()
{
string result = await GetInfoAsync("AsyncVoid", 2);
Console.WriteLine(result);
}
async static Task<string> GetInfoAsync(string name,int seconds)
{
await Task.Delay(TimeSpan.FromSeconds(seconds));
if(name.Contains("Exception"))
throw new Exception(string.Format("Boom from {0}!",name));
return string.Format("Task {0} is running on a thread id{1}. Is thread pool thread: {2}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
}
- 在
Main
方法内部添加以下代码片段:
Task t = AsyncTask();
t.Wait();
AsyncVoid();
Thread.Sleep(TimeSpan.FromSeconds(3));
t = AsyncTaskWithErrors();
while(!t.IsFaulted)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
}
Console.WriteLine(t.Exception);
//try
//{
// AsyncVoidWithErrors();
// Thread.Sleep(TimeSpan.FromSeconds(3));
//}
//catch (Exception ex)
//{
// Console.WriteLine(ex);
//}
//int[] numbers = new[] {1, 2, 3, 4, 5};
//Array.ForEach(numbers, async number => {
// await Task.Delay(TimeSpan.FromSeconds(1));
// if (number == 3) throw new Exception("Boom!");
// Console.WriteLine(number);
//});
Console.ReadLine();
- 运行程序。
它是如何工作的...
程序启动时,我们通过调用两个方法AsyncTask
和AsyncVoid
启动了两个异步操作。第一个方法返回一个Task
对象,而另一个返回的是async void
,因为它没有返回值。它们都立即返回,因为它们是异步的,但是第一个可以通过返回的任务状态轻松监视,或者只需调用其上的Wait
方法。等待第二个方法完成的唯一方法是真正等待一段时间,因为我们没有声明任何可以用来监视异步操作状态的对象。当然,可以使用某种共享状态变量,并从async void
方法中设置它,同时从调用
方法中检查它,但最好还是返回一个Task
对象。
最危险的部分是异常处理。在async void
方法的情况下,异常处理方法将被发布到当前同步上下文;在我们的情况下,是线程池。线程池上的未处理异常将终止整个进程。可以使用AppDomain.UnhandledException
事件拦截未处理的异常,但没有办法从那里恢复进程。要体验这一点,我们应该取消注释Main
方法内部的try
/catch
块,然后运行程序。
关于使用async void
lambda 表达式的另一个事实:它们与广泛使用的标准.NET Framework 类库中的Action
类型兼容。很容易忘记在此 lambda 内部进行异常处理,这将再次使程序崩溃。要查看此示例,请取消注释Main
方法内部的第二个已注释的块。
我强烈建议只在 UI 事件处理程序中使用async void
。在所有其他情况下,请使用返回Task
的方法。
设计自定义可等待类型
本示例展示了如何设计一个非常基本的可等待类型,与await
运算符兼容。
准备工作
要执行此示例,您需要 Visual Studio 2012。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter5\Recipe8
中找到。
如何做...
要设计自定义可等待类型,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中,添加以下using
指令:
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
async static Task AsynchronousProcessing()
{
var sync = new CustomAwaitable(true);
string result = await sync;
Console.WriteLine(result);
var async = new CustomAwaitable(false);
result = await async;
Console.WriteLine(result);
}
class CustomAwaitable
{
public CustomAwaitable(bool completeSynchronously)
{
_completeSynchronously = completeSynchronously;
}
public CustomAwaiter GetAwaiter()
{
return new CustomAwaiter(_completeSynchronously);
}
private readonly bool _completeSynchronously;
}
class CustomAwaiter : INotifyCompletion
{
private string _result = "Completed synchronously";
private readonly bool _completeSynchronously;
public bool IsCompleted { get {return _completeSynchronously; } }
public CustomAwaiter(bool completeSynchronously)
{
_completeSynchronously = completeSynchronously;
}
public string GetResult()
{
return _result;
}
public void OnCompleted(Action continuation)
{
ThreadPool.QueueUserWorkItem( state => {
Thread.Sleep(TimeSpan.FromSeconds(1));
_result = GetInfo();
if(continuation != null) continuation();
});
}
private string GetInfo()
{
return string.Format("Task is running on a thread id{0}. Is thread pool thread: {1}", name,Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
}
}
- 在
Main
方法内部添加以下代码片段:
Task t = AsynchronousProcessing();
t.Wait();
- 运行程序。
它是如何工作的...
为了与await
运算符兼容,类型应符合 C# 5.0 规范中规定的一些要求。如果您已安装 Visual Studio 2012,则可以在C:\Program Files\Microsoft Visual Studio 11.0\VC#\Specifications\1033
文件夹中找到规范文档(假设您已使用默认安装路径)。
在第 7.7.7.1 段中,我们找到了可等待表达式的定义:
等待表达式的任务需要是可等待的。如果表达式 t 是可等待的,则满足以下条件之一:
-
t 是动态的编译时类型
-
t 具有一个名为 GetAwaiter 的可访问的实例或扩展方法,没有参数和类型参数,并且返回类型 A,对于该类型,满足以下所有条件:
-
A 实现了接口 System.Runtime.CompilerServices.INotifyCompletion(以下简称 INotifyCompletion)
-
A 具有可访问的、可读的 bool 类型的 IsCompleted 实例属性
-
A 具有一个名为 GetResult 的可访问的实例方法,没有参数和类型参数
这些信息足以让我们开始。首先,我们定义一个可等待类型CustomAwaitable
并实现GetAwaiter
方法,该方法反过来返回CustomAwaiter
类型的实例。CustomAwaiter
实现了INotifyCompletion
接口;具有bool
类型的IsCompleted
属性,并且具有GetResult
方法,该方法返回string
类型。最后,我们编写了一段代码,创建了两个CustomAwaitable
对象,并等待它们两个。
现在我们应该了解await
表达式的评估方式。这次,为了避免不必要的细节,规范没有被引用。基本上,如果IsCompleted
属性返回true
,我们只需同步调用GetResult
方法。这样,如果操作已经完成,我们就不需要为异步任务执行分配资源。我们通过向CustomAwaitable
对象的构造方法提供completeSynchronously
参数来覆盖这种情况。
否则,我们将一个回调操作注册到CustomAwaiter
的OnCompleted
方法,并启动异步操作。当它完成时,它将调用提供的回调,该回调将通过在CustomAwaiter
对象上调用GetResult
方法来获取结果。
注意
此实现仅用于教育目的。每当编写异步函数时,最自然的方法是使用标准的Task
类型。只有在您无法使用Task
并且确切知道自己在做什么的情况下,才应该定义自己的可等待类型。
还有许多其他与设计自定义可等待类型相关的主题,例如ICriticalNotifyCompletion
接口实现和同步上下文传播。在了解了可等待类型的基本设计原理之后,您将能够使用 C#语言规范和其他信息源轻松找到所需的详细信息。但我想强调的是,除非您有非常充分的理由,否则请使用Task
类型。
使用动态类型与等待
这个示例展示了如何设计一个与await
运算符和动态 C#类型兼容的非常基本的类型。
准备工作
要按照这个示例进行操作,您需要 Visual Studio 2012。您需要互联网访问以下载 NuGet 包。没有其他先决条件。此示例的源代码可以在BookSamples\Chapter5\Recipe9
中找到。
如何做...
要了解如何使用dynamic
类型与await
,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
通过以下步骤添加对ImpromptuInterface NuGet 包的引用:
-
在项目中右键单击引用文件夹,然后选择管理 NuGet 包...菜单选项。
-
现在将您喜欢的引用添加到ImpromptuInterface NuGet包中。您可以使用管理 NuGet 包对话框中的搜索功能,如下所示:
- 在
Program.cs
文件中,使用以下using
指令:
using System;
using System.Dynamic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ImpromptuInterface;
- 在
Main
方法下面添加以下代码片段:
async static Task AsynchronousProcessing()
{
string result = await GetDynamicAwaitableObject(true);
Console.WriteLine(result);
result = await GetDynamicAwaitableObject(false);
Console.WriteLine(result);
}
static dynamic GetDynamicAwaitableObject(bool completeSynchronously)
{
dynamic result = new ExpandoObject();
dynamic awaiter = new ExpandoObject();
awaiter.Message = "Completed synchronously";
awaiter.IsCompleted = completeSynchronously;
awaiter.GetResult = (Func<string>)(() => awaiter.Message);
awaiter.OnCompleted = (Action<Action>) ( callback =>
ThreadPool.QueueUserWorkItem(state => {
Thread.Sleep(TimeSpan.FromSeconds(1));
awaiter.Message = GetInfo();
if (callback != null) callback();
})
);
IAwaiter<string> proxy = Impromptu.ActLike(awaiter);
result.GetAwaiter = (Func<dynamic>) ( () => proxy );
return result;
}
static string GetInfo()
{
return string.Format("Task is running on a thread id {0}. Is thread pool thread: {1}",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
public interface IAwaiter<T> : INotifyCompletion
{
bool IsCompleted { get; }
T GetResult();
}
- 在
Main
方法内部添加以下代码片段:
Task t = AsynchronousProcessing();
t.Wait();
- 运行程序。
工作原理...
在这里,我们重复了上一个示例中的技巧,但这次是借助动态表达式的帮助。我们可以通过 NuGet 来实现这个目标——一个包管理器,其中包含许多有用的库。这次我们将使用一个动态创建包装器并实现我们需要的接口的库。
首先,我们创建两个ExpandoObject
类型的实例,并将它们分配给动态局部变量。这些变量将是我们的 awaitable 和 awaiter 对象。由于 awaitable 对象只需要具有GetAwaiter
方法,因此提供它没有问题。ExpandoObject
与dynamic
关键字结合使用,允许我们自定义它,并通过分配相应的值添加属性和方法。实际上,它是一种具有string
类型键和object
类型值的字典类型集合。如果您熟悉 JavaScript 编程语言,您可能会注意到这与 JavaScript 对象非常相似。
由于dynamic
允许我们在 C#中跳过编译时检查,ExpandoObject
是这样编写的,如果您将某些内容分配给属性,它会创建一个字典条目,其中键是属性名称,值是提供的任何值。当您尝试获取属性值时,它会进入字典并提供存储在相应字典条目中的值。如果值是Action
或Func
类型,我们实际上存储了一个委托,该委托反过来可以像方法一样使用。因此,dynamic
类型与ExpandoObject
的组合允许我们创建一个对象,并动态为其提供属性和方法。
现在,我们需要构建我们的 awaiter 和 awaitable 对象。让我们从 awaiter 开始。首先,我们提供一个名为Message
的属性,并为该属性提供一个初始值。然后,我们定义GetResult
方法,使用Func<string>
类型,我们分配一个 lambda 表达式,该表达式返回Message
属性的值。接下来,我们实现IsCompleted
属性。如果设置为true
,我们可以跳过其余的工作并继续进行我们的 awaitable 对象,存储在result
局部变量中。我们只需要添加一个返回dynamic
对象的方法,并从中返回我们的 awaiter。然后,我们可以使用result
作为 await 表达式;但是,它将以同步方式运行。
主要挑战是在我们的动态对象上实现异步处理。C#语言规范规定 awaiter 必须实现INotifyCompletion
或ICriticalNotifyCompletion
接口,而ExpandoObject
并没有这样做。即使我们动态实现OnCompleted
方法,并将其添加到 awaiter 对象中,我们也不会成功,因为我们的对象没有实现上述任何接口。
为了解决这个问题,我们使用了从 NuGet 获取的ImpromptuInterface
库。它允许我们使用Impromptu.ActLike
方法动态创建代理对象,这些对象将实现所需的接口。如果我们尝试创建一个实现INotifyCompletion
接口的代理,我们仍然会失败,因为代理对象不再是动态的,而这个接口只有OnCompleted
方法,但没有IsCompleted
属性或GetResult
方法。作为最后的解决方法,我们定义了一个通用接口IAwaiter<T>
,它实现了INotifyCompletion
并添加了所有必需的属性和方法。现在,我们将其用于代理生成,并将result
对象更改为从GetAwaiter
方法返回代理而不是 awaiter。程序现在可以工作了;我们刚刚构建了一个在运行时完全动态的可等待对象。
第六章:使用并发集合
在本章中,我们将浏览包含在.NET Framework 基类库中的并发编程的不同数据结构。您将学习以下内容:
-
使用并发字典
-
使用并发队列实现异步处理
-
使用并发堆栈改变异步处理顺序
-
使用并发包创建可扩展的网络爬虫
-
使用阻塞集合泛化异步处理
介绍
编程需要理解和掌握基本的数据结构和算法。为了选择最适合并发情况的数据结构,程序员必须了解许多事情,比如算法时间、空间复杂度和大 O 符号。在不同的知名场景中,我们总是知道哪些数据结构更有效。
对于并发计算,我们需要适当的数据结构。这些数据结构必须是可扩展的,在可能的情况下避免锁,并且同时提供线程安全的访问。自.NET Framework 4 以来,具有几种数据结构的System.Collections.Concurrent
命名空间。在本章中,我们将涵盖几种数据结构,并展示如何使用它们的非常简单的示例。
让我们从ConcurrentQueue
开始。这个集合使用原子比较和交换(CAS)操作和SpinWait
来确保线程安全。它实现了一个先进先出(FIFO)集合,这意味着项目以它们被添加到队列的顺序出队。要向队列添加项目,您调用Enqueue
方法。TryDequeue
方法尝试从队列中取出第一个项目,TryPeek
方法尝试获取第一个项目而不从队列中移除它。
ConcurrentStack
也是使用 CAS 操作而没有使用任何锁来实现的。它是一个后进先出(LIFO)集合,这意味着最近添加的项目将首先返回。要添加项目,您可以使用Push
和PushRange
方法,要检索,您可以使用TryPop
和TryPopRange
,要检查,您可以使用TryPeek
方法。
ConcurrentBag
是一个支持重复项目的无序集合。它针对多个线程以每个线程产生和消耗自己的任务的方式进行分区的场景进行了优化,很少处理其他线程的任务(在这种情况下,它使用锁)。您可以使用Add
方法向包中添加项目,使用TryPeek
进行检查,并使用TryTake
方法进行获取。
注意
请避免在提到的集合上使用Count
属性。它们使用链表实现,而Count
是一个O(N)
操作。如果您需要检查集合是否为空,请使用IsEmpty
属性,这是一个O(1)
操作。
ConcurrentDictionary
是一个线程安全的字典集合实现。它对读操作是无锁的。但是,它对写操作需要锁定。并发字典使用多个锁,实现了对字典桶的细粒度锁定模型。锁的数量可以通过使用带有参数concurrencyLevel
的构造函数来定义,这意味着估计数量的线程将同时更新字典。
注意
由于并发字典使用锁定,有许多操作需要在字典内部获取所有锁。请避免在不需要的情况下使用这些操作。它们是:Count
,IsEmpty
,Keys
,Values
,CopyTo
和ToArray
。
BlockingCollection
是IProducerConsumerCollection
泛型接口实现的高级包装器。它具有许多更先进的功能,并且在实现管道场景时非常有用,当您有一些步骤使用了处理前一步骤结果时。BlockingCollection
类支持阻塞、限制内部集合容量、取消集合操作以及从多个阻塞集合中检索值等功能。
并发算法可能非常复杂,覆盖所有并发集合——无论是更先进还是更简单——都需要编写一本单独的书。在这里,我们只展示了使用并发集合的最简单的例子。
使用 ConcurrentDictionary
这个示例展示了一个非常简单的场景,在单线程环境中比较了普通字典集合与并发字典的性能。
准备工作
要按照这个示例,您需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter6\Recipe1
中找到。
如何做...
为了理解普通字典集合与并发字典集合性能差异,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
- 在
Main
方法下面添加以下代码片段:
const string Item = "Dictionary item";
public static string CurrentItem;
- 在
Main
方法中添加以下代码片段:
var concurrentDictionary = new ConcurrentDictionary<int, string>();
var dictionary = new Dictionary<int, string>();
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 1000000; i++)
{
lock (dictionary)
{
dictionary[i] = Item;
}
}
sw.Stop();
Console.WriteLine("Writing to dictionary with a lock: {0}", sw.Elapsed);
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
concurrentDictionary[i] = Item;
}
sw.Stop();
Console.WriteLine("Writing to a concurrent dictionary: {0}", sw.Elapsed);
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
lock (dictionary)
{
CurrentItem = dictionary[i];
}
}
sw.Stop();
Console.WriteLine("Reading from dictionary with a lock: {0}", sw.Elapsed);
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
CurrentItem = concurrentDictionary[i];
}
sw.Stop();
Console.WriteLine("Reading from a concurrent dictionary: {0}", sw.Elapsed);
- 运行程序。
它是如何工作的...
当程序启动时,我们创建了两个集合。其中一个是标准字典集合,另一个是一个新的并发字典。然后我们开始添加到它,使用带锁的标准字典并测量一百万次迭代完成所需的时间。然后我们测量在相同情况下ConcurrentDictionary
的性能,最后比较从两个集合中检索值的性能。
在这个非常简单的场景中,我们发现ConcurrentDictionary
在写操作上比普通的带锁的字典慢得多,但在检索操作上更快。因此,如果我们需要从字典中进行许多线程安全的读取,ConcurrendDictionary
集合是最佳选择。
注意
如果您只需要对字典进行只读、多线程访问,可能不需要执行线程安全读取。在这种情况下,最好只使用普通字典或ReadOnlyDictionary
集合。
ConcurrentDictionary
是使用细粒度锁定技术实现的,这使得它在多次写入时比使用带锁的常规字典更好地扩展(称为粗粒度锁定)。正如我们在这个例子中看到的,当我们只使用一个线程时,并发字典要慢得多,但当我们将其扩展到五六个线程时(如果我们有足够的 CPU 核心可以同时运行它们),并发字典实际上会表现得更好。
使用 ConcurrentQueue 实现异步处理
这个示例将展示一个创建一组任务,由多个工作线程异步处理的示例。
准备工作
要按照这个示例,您需要 Visual Studio 2012。没有其他先决条件。这个示例的源代码可以在BookSamples\Chapter6\Recipe2
中找到。
如何做...
为了理解创建一组任务,由多个工作线程异步处理的工作原理,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static async Task RunProgram()
{
var taskQueue = new ConcurrentQueue<CustomTask>();
var cts = new CancellationTokenSource();
var taskSource = Task.Run(() => TaskProducer(taskQueue));
Task[] processors = new Task[4];
for (int i = 1; i <= 4; i++)
{
string processorId = i.ToString();
processors[i-1] = Task.Run(
() => TaskProcessor(taskQueue, "Processor " + processorId, cts.Token));
}
await taskSource;
cts.CancelAfter(TimeSpan.FromSeconds(2));
await Task.WhenAll(processors);
}
static async Task TaskProducer(ConcurrentQueue<CustomTask> queue)
{
for (int i = 1; i <= 20; i++)
{
await Task.Delay(50);
var workItem = new CustomTask {Id = i};
queue.Enqueue(workItem);
Console.WriteLine("Task {0} has been posted", workItem.Id);
}
}
static async Task TaskProcessor(ConcurrentQueue<CustomTask> queue, string name, CancellationToken token){
CustomTask workItem;
bool dequeueSuccesful = false;
await GetRandomDelay();
do
{
dequeueSuccesful = queue.TryDequeue(out workItem);
if (dequeueSuccesful)
{
Console.WriteLine("Task {0} has been processed by {1}", workItem.Id, name);
}
await GetRandomDelay();
}
while (!token.IsCancellationRequested);
}
static Task GetRandomDelay()
{
int delay = new Random(DateTime.Now.Millisecond).Next(1, 500);
return Task.Delay(delay);
}
class CustomTask
{
public int Id { get; set; }
}
- 在
Main
方法中添加以下代码片段:
Task t = RunProgram();
t.Wait();
- 运行程序。
它是如何工作的...
程序运行时,我们使用ConcurrentQueue
集合创建了一个任务队列。然后我们创建了一个取消标记,用于在我们将任务发布到队列后停止工作。接下来,我们启动一个单独的工作者线程,将任务发布到任务队列。这部分产生了我们异步处理的工作负载。
现在让我们定义程序的任务消耗部分。我们创建四个工作者,它们将等待一段随机时间,然后从任务队列获取一个任务,处理它,并重复整个过程,直到我们发出取消标记。最后,我们启动任务生成线程,等待其完成,然后使用取消标记向消费者发出我们完成工作的信号。最后一步是等待所有消费者完成。
我们看到我们有任务从头到尾处理,但可能会出现一个后续任务在较早的任务之前被处理,因为我们有四个独立运行的工作者,任务处理时间不是恒定的。我们看到队列的访问是线程安全的;没有工作项被重复获取。
更改异步处理顺序 ConcurrentStack
这个食谱是对上一个的轻微修改。我们将再次创建一组任务,由多个工作者异步处理,但这次我们使用ConcurrentStack
来实现,并看到其中的区别。
准备工作
要按照这个食谱进行操作,你需要 Visual Studio 2012。没有其他先决条件。这个食谱的源代码可以在BookSamples\Chapter6\Recipe3
中找到。
如何做...
为了理解使用ConcurrentStack
实现的一组任务的处理,执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static async Task RunProgram()
{
var taskStack = new ConcurrentStack<CustomTask>();
var cts = new CancellationTokenSource();
var taskSource = Task.Run(() => TaskProducer(taskStack));
Task[] processors = new Task[4];
for (int i = 1; i <= 4; i++)
{
string processorId = i.ToString();
processors[i - 1] = Task.Run(
() => TaskProcessor(taskStack, "Processor " + processorId, cts.Token));
}
await taskSource;
cts.CancelAfter(TimeSpan.FromSeconds(2));
await Task.WhenAll(processors);
}
static async Task TaskProducer(ConcurrentStack<CustomTask> stack)
{
for (int i = 1; i <= 20; i++)
{
await Task.Delay(50);
var workItem = new CustomTask { Id = i };
stack.Push(workItem);
Console.WriteLine("Task {0} has been posted", workItem.Id);
}
}
static async Task TaskProcessor(
ConcurrentStack<CustomTask> stack, string name, CancellationToken token)
{
await GetRandomDelay();
do
{
CustomTask workItem;
bool popSuccesful = stack.TryPop(out workItem);
if (popSuccesful)
{
Console.WriteLine("Task {0} has been processed by {1}", workItem.Id, name);
}
await GetRandomDelay();
}
while (!token.IsCancellationRequested);
}
static Task GetRandomDelay()
{
int delay = new Random(DateTime.Now.Millisecond).Next(1, 500);
return Task.Delay(delay);
}
class CustomTask
{
public int Id { get; set; }
}
- 在
Main
方法中添加以下代码片段:
Task t = RunProgram();
t.Wait();
- 运行程序。
它是如何工作的...
当程序运行时,我们现在创建了ConcurrentStack
集合的一个实例。其余部分几乎与上一个食谱相同,只是在并发栈上使用Push
和TryPop
方法的地方,我们在并发队列上使用Enqueue
和TryDequeue
。
现在我们看到任务处理顺序已经改变。栈是一个后进先出的集合,工作者首先处理后续任务。在并发队列的情况下,任务几乎按照它们被添加的顺序进行处理。这意味着根据工作者的数量,我们肯定会在给定的时间范围内处理首先创建的任务。在栈的情况下,较早创建的任务优先级较低,可能在生产者停止向栈中添加更多任务之前不会被处理。这种行为非常特殊,最好在这种情况下使用队列。
使用 ConcurrentBag 创建可扩展的爬虫
这个食谱展示了如何在多个独立的工作者之间分配工作负载,他们既生产工作,又处理工作。
准备工作
要按照这个食谱进行操作,你需要 Visual Studio 2012。没有其他先决条件。这个食谱的源代码可以在BookSamples
的\Chapter6\Recipe4
中找到。
如何做...
以下步骤演示了如何在多个独立的工作者之间分配工作负载,他们既生产工作,又处理工作:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static Dictionary<string, string[]> _contentEmulation = new Dictionary<string, string[]>();
static async Task RunProgram()
{
var bag = new ConcurrentBag<CrawlingTask>();
string[] urls = new[] {"http://microsoft.com/", "http://google.com/", "http://facebook.com/", "http://twitter.com/"};
var crawlers = new Task[4];
for (int i = 1; i <= 4; i++)
{
string crawlerName = "Crawler " + i.ToString();
bag.Add(new CrawlingTask { UrlToCrawl = urls[i-1], ProducerName = "root"});
crawlers[i - 1] = Task.Run(() => Crawl(bag, crawlerName));
}
await Task.WhenAll(crawlers);
}
static async Task Crawl(ConcurrentBag<CrawlingTask> bag, string crawlerName)
{
CrawlingTask task;
while (bag.TryTake(out task))
{
IEnumerable<string> urls = await GetLinksFromContent(task);
if (urls != null)
{
foreach (var url in urls)
{
var t = new CrawlingTask
{
UrlToCrawl = url,
ProducerName = crawlerName
};
bag.Add(t);
}
}
Console.WriteLine("Indexing url {0} posted by {1} is completed by {2}!",
task.UrlToCrawl, task.ProducerName, crawlerName);
}
}
static async Task<IEnumerable<string>> GetLinksFromContent(CrawlingTask task)
{
await GetRandomDelay();
if (_contentEmulation.ContainsKey(task.UrlToCrawl)) return _contentEmulation[task.UrlToCrawl];
return null;
}
static void CreateLinks()
{
_contentEmulation["http://microsoft.com/"] = new [] { "http://microsoft.com/a.html", "http://microsoft.com/b.html" };
_contentEmulation["http://microsoft.com/a.html"] = new[] { "http://microsoft.com/c.html", "http://microsoft.com/d.html" };
_contentEmulation["http://microsoft.com/b.html"] = new[] { "http://microsoft.com/e.html" };
_contentEmulation["http://google.com/"] = new[] { "http://google.com/a.html", "http://google.com/b.html" };
_contentEmulation["http://google.com/a.html"] = new[] { "http://google.com/c.html", "http://google.com/d.html" };
_contentEmulation["http://google.com/b.html"] = new[] { "http://google.com/e.html", "http://google.com/f.html" };
_contentEmulation["http://google.com/c.html"] = new[] { "http://google.com/h.html", "http://google.com/i.html" };
_contentEmulation["http://facebook.com/"] = new [] { "http://facebook.com/a.html", "http://facebook.com/b.html" };
_contentEmulation["http://facebook.com/a.html"] = new[] { "http://facebook.com/c.html", "http://facebook.com/d.html" };
_contentEmulation["http://facebook.com/b.html"] = new[] { "http://facebook.com/e.html" };
_contentEmulation["http://twitter.com/"] = new[] { "http://twitter.com/a.html", "http://twitter.com/b.html" };
_contentEmulation["http://twitter.com/a.html"] = new[] { "http://twitter.com/c.html", "http://twitter.com/d.html" };
_contentEmulation["http://twitter.com/b.html"] = new[] { "http://twitter.com/e.html" };
_contentEmulation["http://twitter.com/c.html"] = new[] { "http://twitter.com/f.html", "http://twitter.com/g.html" };
_contentEmulation["http://twitter.com/d.html"] = new[] { "http://twitter.com/h.html" };
_contentEmulation["http://twitter.com/e.html"] = new[] { "http://twitter.com/i.html" };
}
static Task GetRandomDelay()
{
int delay = new Random(DateTime.Now.Millisecond).Next(150, 200);
return Task.Delay(delay);
}
class CrawlingTask
{
public string UrlToCrawl { get; set; }
public string ProducerName { get; set; }
}
- 在
Main
方法中添加以下代码片段:
CreateLinks();
Task t = RunProgram();
t.Wait();
- 运行程序。
它是如何工作的...
该程序模拟了多个网络爬虫进行网页索引。网络爬虫是一个打开网页并索引内容的程序,并尝试访问该页面包含的所有链接,并索引这些链接页面。一开始,我们定义了一个包含不同网页 URL 的字典。这个字典模拟了包含指向其他页面链接的网页。实现非常天真;它不关心已经访问过的页面,但它很简单,可以让我们专注于并发工作负载。
然后我们创建一个包含爬行任务的并发包。我们创建四个爬虫,并为每个爬虫提供不同的站点根 URL。然后,我们等待所有爬虫竞争。现在,每个爬虫开始索引它所给定的站点 URL。我们通过等待一些随机时间来模拟网络 I/O 过程;然后,如果页面包含更多的 URL,爬虫会将更多的爬行任务发布到包中。然后,它检查包中是否还有任何任务需要爬行。如果没有,爬虫就完成了。
如果我们检查前四行下面的输出,这些行是根 URL,我们会发现通常由爬虫编号N发布的任务会被同一个爬虫处理。然而,后面的行会有所不同。这是因为内部ConcurrentBag
针对有多个线程同时添加和删除项目的情况进行了优化。这是通过让每个线程使用自己的本地项目队列来实现的,因此,在这个队列被占用时,我们不需要任何锁。只有当本地队列中没有项目时,我们才会执行一些锁定,并尝试从另一个线程的本地队列中“窃取”工作。这种行为有助于在所有工作者之间分配工作并避免锁定。
使用 BlockingCollection 泛化异步处理
本示例将描述如何使用BlockingCollection
来简化工作负载异步处理的实现。
准备工作
要执行此示例,您需要 Visual Studio 2012。不需要其他先决条件。此示例的源代码可以在BookSamples\Chapter6\Recipe5
中找到。
如何做...
要理解BlockingCollection
如何简化工作负载异步处理的实现,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C#控制台应用程序项目。
-
在
Program.cs
文件中添加以下using
指令:
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
- 在
Main
方法下面添加以下代码片段:
static async Task RunProgram(IProducerConsumerCollection<CustomTask> collection = null)
{
var taskCollection = new BlockingCollection<CustomTask>();
if (null != collection)
taskCollection= new BlockingCollection<CustomTask>(collection);
var taskSource = Task.Run(() => TaskProducer(taskCollection));
Task[] processors = new Task[4];
for (int i = 1; i <= 4; i++)
{
string processorId = "Processor " + i;
processors[i - 1] = Task.Run(() => TaskProcessor(taskCollection, processorId));
}
await taskSource;
await Task.WhenAll(processors);
}
static async Task TaskProducer(BlockingCollection<CustomTask> collection)
{
for (int i = 1; i <= 20; i++)
{
await Task.Delay(20);
var workItem = new CustomTask { Id = i };
collection.Add(workItem);
Console.WriteLine("Task {0} have been posted", workItem.Id);
}
collection.CompleteAdding();
}
static async Task TaskProcessor(BlockingCollection<CustomTask> collection, string name)
{
await GetRandomDelay();
foreach (CustomTask item in collection.GetConsumingEnumerable())
{
Console.WriteLine("Task {0} have been processed by {1}", item.Id, name);
await GetRandomDelay();
}
}
static Task GetRandomDelay()
{
int delay = new Random(DateTime.Now.Millisecond).Next(1, 500);
return Task.Delay(delay);
}
class CustomTask
{
public int Id { get; set; }
}
- 在
Main
方法中添加以下代码片段:
Console.WriteLine("Using a Queue inside of BlockingCollection");
Console.WriteLine();
Task t = RunProgram();
t.Wait();
Console.WriteLine();
Console.WriteLine("Using a Stack inside of BlockingCollection");
Console.WriteLine();
t = RunProgram(new ConcurrentStack<CustomTask>());
t.Wait();
- 运行程序。
它是如何工作的...
这里我们正好采用了第一种情况,但现在我们使用了一个提供许多有用好处的BlockingCollection
类。首先,我们能够改变任务在阻塞集合中存储的方式。默认情况下,它使用ConcurrentQueue
容器,但我们可以使用任何实现IProducerConsumerCollection
泛型接口的集合。为了说明这一点,我们运行程序两次,第二次使用ConcurrentStack
作为底层集合。
工作者通过迭代阻塞集合上的GetConsumingEnumerable
方法调用结果来获取工作项。如果集合中没有项目,迭代器将阻塞工作者线程,直到有项目发布到集合中。当生产者在集合上调用CompleteAdding
方法时,循环结束。这表示工作已完成。
注意
很容易犯一个错误,只是迭代BlockingCollection
,因为它本身实现了IEnumerable
。不要忘记使用GetConsumingEnumerable
,否则你将只是迭代集合的“快照”,并得到完全意想不到的程序行为。
工作负载生产者将任务插入BlockingCollection
,然后调用CompleteAdding
方法,导致所有工作人员完成。现在在程序输出中,我们看到两个结果序列,说明并发队列和堆栈集合之间的区别。
第七章:使用 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 来使我们的程序易于阅读和维护。
第十一章:还有更多
在这一章中,我们将看到 Windows 8 操作系统中的一种新的编程范式。您将学习到:
-
在 Windows Store 应用程序中使用定时器
-
从通常的应用程序中使用 WinRT
-
在 Windows Store 应用程序中使用 BackgroundTask
介绍
微软于 2011 年 9 月 13 日在 BUILD 大会上发布了 Windows 8 的第一个公共测试版。新操作系统试图通过引入响应式 UI、适用于触摸的平板设备、低功耗、新的应用程序模型、新的异步 API 和更严格的安全性等功能来解决 Windows 几乎所有的问题。
Windows API 改进的核心是一个新的多平台组件系统WinRT,它是 COM 的逻辑发展。使用 WinRT,程序员可以使用本机 C++代码、C#和.NET,甚至 JavaScript 和 HTML 来开发应用程序。另一个变化是引入了一个集中的应用商店,这在 Windows 平台之前是不存在的。
作为一个新的应用程序平台,Windows 8 具有向后兼容性,并允许运行通常的 Windows 应用程序。这导致了两类主要的应用程序:Windows Store 应用程序,其中新程序通过 Windows Store 分发,以及自上一版本 Windows 以来没有改变的通常的经典应用程序。
在本章中,我们将看到 Windows Store 应用程序。开发范式发生了很大的变化,作为程序员,您必须遵守特定的规则。程序必须在有限的时间内响应启动或完成,保持整个操作系统和其他应用程序的响应。为了节省电池,您的应用程序不再默认在后台运行;而是被暂停并实际停止执行。
新的 Windows API 是异步的,您只能在应用程序中使用白名单 API 函数。例如,您不允许再创建一个Thread
类实例。您必须使用系统管理的线程池。许多通常的 API 不再能够使用,您必须学习以前实现相同目标的新方法。
在这一章中,我们将看到 Windows Store 应用程序与通常的 Windows 应用程序有何不同,我们如何可以在通常的应用程序中使用一些 WinRT 的好处,并通过一个简化的具有后台通知的 Windows Store 应用程序的场景。
在 Windows Store 应用程序中使用定时器
这个示例展示了如何在 Windows Store 应用程序中使用一个简单的定时器。
准备就绪
要完成这个示例,您需要 Visual Studio 2012 和 Windows 8+操作系统。不需要其他先决条件。此示例的源代码可以在7644_Code\Chapter11\Recipe1
中找到。
如何做...
要理解如何在 Windows Store 应用程序中使用定时器,请执行以下步骤:
-
启动 Visual Studio 2012。在Windows Store下创建一个新的 C# 空白应用(XAML)项目。
-
如果要求您更新开发人员许可证,您必须同意微软的隐私条款。
-
然后,登录您的 Microsoft 账户(或首先创建一个)。
-
最后,您会收到一个确认对话框,开发人员许可证已成功更新。
-
在
MainPage.xaml
文件中,为Grid
元素添加Name
属性:
<Grid Name="Grid" Background="{StaticResourceApplicationPageBackgroundThemeBrush}">
- 在
MainPage.xaml.cs
文件中,添加以下using
指令:
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
- 在
MainPage
构造函数定义之前添加以下代码片段:
private readonly DispatcherTimer _timer;
private int _ticks;
- 用以下代码片段替换
MainPage()
构造函数:
public MainPage()
{
InitializeComponent();
_timer = new DispatcherTimer();
_ticks = 0;
}
- 在
OnNavigatedTo
方法中添加以下代码片段:
Grid.Children.Clear();
var commonPanel = new StackPanel
{
Orientation = Orientation.Vertical,
HorizontalAlignment = HorizontalAlignment.Center
};
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Center
};
var textBlock = new TextBlock
{
Text = "Sample timer application",
FontSize = 32,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(40)
};
var timerTextBlock = new TextBlock
{
Text = "0",
FontSize = 32,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(40)
};
var timerStateTextBlock = new TextBlock
{
Text = "Timer is enabled",
FontSize = 32,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(40)
};
var startButton = new Button { Content = "Start",FontSize = 32};
var stopButton = new Button { Content = "Stop",FontSize = 32};
buttonPanel.Children.Add(startButton);
buttonPanel.Children.Add(stopButton);
commonPanel.Children.Add(textBlock);
commonPanel.Children.Add(timerTextBlock);
commonPanel.Children.Add(timerStateTextBlock);
commonPanel.Children.Add(buttonPanel);
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (sender, eventArgs) =>
{
timerTextBlock.Text = _ticks.ToString(); _ticks++;
};
_timer.Start();
startButton.Click += (sender, eventArgs) =>
{
timerTextBlock.Text = "0";
_timer.Start();
_ticks = 1;
timerStateTextBlock.Text = "Timer is enabled";
};
stopButton.Click += (sender, eventArgs) =>
{
_timer.Stop();
timerStateTextBlock.Text = "Timer is disabled";
};
Grid.Children.Add(commonPanel);
- 运行程序。
它是如何工作的...
当程序运行时,它会创建一个MainPage
类的实例。在构造函数中,我们实例化了DispatcherTimer
,并将ticks
计数器初始化为零。然后,在OnNavigatedTo
事件处理程序中,我们创建了 UI 控件,并将开始和停止按钮绑定到相应的 lambda 表达式,其中包含start
和stop
逻辑。
如您所见,timer
事件处理程序直接与 UI 控件一起工作。这是可以的,因为DispatcherTimer
是以这样一种方式实现的,即timer
的Tick
事件的处理程序由 UI 线程运行。然而,如果您运行程序然后切换到其他内容,并在几分钟后切换到程序删除,您可能会注意到秒表比实际经过的时间要慢得多。这是因为 Windows 8 应用程序,或者通常被称为 Windows Store 应用程序,具有完全不同的生命周期。
注意
请注意,Windows Store 应用程序的行为与智能手机和平板电脑平台上的应用程序非常相似。它们在后台运行而不是在一段时间后变得暂停,这意味着它们实际上被冻结,直到用户切换回它们。在它们变得暂停之前,您有有限的时间来保存当前应用程序状态,并且在应用程序再次运行时能够恢复状态。
虽然这种行为可以节省电源和 CPU 资源,但它会给那些应该在后台进行一些处理的应用程序的编程带来重大困难。Windows 8 有一组特殊的 API 用于编程这些应用程序。我们将在本章后面讨论这样的情景。
使用 WinRT 从普通应用程序中
这个示例展示了如何创建一个控制台应用程序,可以使用 WinRT API。
准备工作
要完成这个示例,您需要 Visual Studio 2012 和 Windows 8+操作系统。没有其他先决条件。此示例的源代码可以在7644_Code\Chapter11\Recipe2
找到。
操作步骤如下:
要了解如何从普通应用程序中使用 WinRT,请执行以下步骤:
-
启动 Visual Studio 2012。创建一个新的 C# 控制台应用程序项目。
-
在 Visual Studio 的解决方案资源管理器中右键单击创建的项目,选择卸载项目...菜单选项。
-
右键单击未加载的项目,选择编辑 ProjectName.csproj菜单选项。
-
在
<TargetFrameworkVersion>
元素下方添加以下 XML:
<TargetPlatformVersion>8.0</TargetPlatformVersion>
-
保存
.csproj
文件,在 Visual Studio 的解决方案资源管理器中右键单击未加载的项目,选择重新加载项目菜单选项。 -
右键单击项目,从Windows下的核心库中选择添加引用。然后点击浏览按钮。
-
导航到
C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5
,然后点击System.Runtime.WindowsRuntime.dll
。 -
在
Program.cs
文件中添加以下using
指令:
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
- 在
Main
方法下方添加以下代码片段:
async static Task AsynchronousProcessing()
{
StorageFolder folder = KnownFolders.DocumentsLibrary;
if (await folder.DoesFileExistAsync("test.txt"))
{
var fileToDelete = await folder.GetFileAsync("test.txt");
await fileToDelete.DeleteAsync(StorageDeleteOption.PermanentDelete);
}
var file = await folder.CreateFileAsync("test.txt",CreationCollisionOption.ReplaceExisting);
using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
using (var writer = new StreamWriter(stream.AsStreamForWrite()))
{
await writer.WriteLineAsync("Test content");
await writer.FlushAsync();
}
using (var stream = await file.OpenAsync(FileAccessMode.Read))
using (var reader = new StreamReader(stream.AsStreamForRead()))
{
string content = await reader.ReadToEndAsync();
Console.WriteLine(content);
}
Console.WriteLine("Enumerating Folder Structure:");
var itemsList = await folder.GetItemsAsync();
foreach (var item in itemsList)
{
if (item is StorageFolder)
{
Console.WriteLine("{0} folder", item.Name);
}
else
{
Console.WriteLine(item.Name);
}
}
}
- 在
Main
方法内添加以下代码片段:
var t = AsynchronousProcessing();
t.GetAwaiter().GetResult();
Console.WriteLine();
Console.WriteLine("Press ENTER to continue");
Console.ReadLine();
- 在
Program
类定义下方添加以下代码片段:
static class Extensions
{
public static async Task<bool> DoesFileExistAsync(thisStorageFolder folder, string fileName)
{
try
{
await folder.GetFileAsync(fileName);
return true;
}
catch (FileNotFoundException)
{
return false;
}
}
}
- 运行程序。
工作原理如下:
在这里,我们使用了一种相当巧妙的方法,从普通的.NET 控制台应用程序中使用 WinRT API。不幸的是,并非所有可用的 API 都适用于这种情况,但仍然可以用于处理运动传感器、GPS 定位服务等。
要在 Visual Studio 中引用 WinRT,我们手动编辑.csproj
文件,指定应用程序的目标平台为 Windows 8。然后我们手动引用System.Runtime.WindowsRuntime.dll
来利用 WinRT 异步操作的GetAwaiter
扩展方法实现。这使我们能够直接在 WinRT API 上使用await
。也有反向转换。当我们创建一个 WinRT 库时,我们必须公开 WinRT 本机IAsyncOperation
接口系列,以便以一种与语言无关的方式从 JavaScript 和 C++中使用异步操作。
WinRT 中的文件操作相当自我描述;这里我们有异步文件创建和删除操作。然而,在 WinRT 中的文件操作包含安全限制,鼓励您使用特殊的 Windows 文件夹来进行应用程序开发,并不允许您使用磁盘驱动器上的任何文件路径。
在 Windows Store 应用程序中使用 BackgroundTask
这个操作指南介绍了在 Windows Store 应用程序中创建后台任务的过程,该任务会在桌面上更新应用程序的动态磁贴。
准备工作
要执行此操作,您需要 Visual Studio 2012 和 Windows 8+操作系统。没有其他先决条件。此操作的源代码可以在7644_Code\Chapter11\Recipe3
中找到。
如何运作...
为了理解如何在 Windows Store 应用程序中使用BackgroundTask
,执行以下步骤:
-
启动 Visual Studio 2012。在Windows Store下创建一个新的 C# 空白应用程序(XAML)项目。如果需要更新开发者许可证,请参考在 Windows Store 应用程序中使用定时器的详细说明。
-
在Assets文件夹中,使用 Paint 编辑器打开SmallLogo.png文件,将其裁剪为 24 x 24 像素大小,将其另存为
SmallLogo-Badge.png
,并将其包含在项目中。 -
打开
Package.appxmanifest
文件。在声明选项卡中,将后台任务添加到支持的声明中。在属性下,检查支持的属性系统事件和定时器,并将入口点的名称设置为YourNamespace.TileSchedulerTask
。YourNamespace
应该是您的应用程序的命名空间。 -
在应用程序 UI选项卡中,将锁定屏幕通知选择为徽章,并将徽章标志选择为Assets\SmallLogo-Badge.png。
-
在
MainPage.xaml
文件中,在Grid
元素中插入以下 XAML:
<StackPanel Margin="50">
<TextBlock Name="Clock"Text="HH:mm"HorizontalAlignment="Center"VerticalAlignment="Center"Style="{StaticResource HeaderTextStyle}"/></StackPanel>
- 在
MainPage.xaml.cs
文件中添加以下using
指令:
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Windows.ApplicationModel.Background;
using Windows.Data.Xml.Dom;
using Windows.System.UserProfile;
using Windows.UI.Notifications;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
- 在
MainPage
构造函数定义之前添加以下代码片段:
private const string TASK_NAME_USERPRESENT ="TileSchedulerTask_UserPresent";
private const string TASK_NAME_TIMER ="TileSchedulerTask_Timer";
private readonly CultureInfo _cultureInfo;
private readonly DispatcherTimer _timer;
- 用以下代码片段替换
MainPage
构造函数:
public MainPage()
{
InitializeComponent();
string language = GlobalizationPreferences.Languages.First();
_cultureInfo = new CultureInfo(language);
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (sender, e) => UpdateClockText();
}
- 在
OnNavigatedTo
方法之前添加以下代码片段:
private void UpdateClockText()
{
Clock.Text = DateTime.Now.ToString(_cultureInfo.DateTimeFormat.FullDateTimePattern);
}
private static async void CreateClockTask()
{
BackgroundAccessStatus result = awaitBackgroundExecutionManager.RequestAccessAsync();
if (result == BackgroundAccessStatus.AllowedMayUseActiveRealTimeConnectivity ||result == BackgroundAccessStatus.AllowedWithAlwaysOnRealTimeConnectivity)
{
TileSchedulerTask.CreateSchedule();
EnsureUserPresentTask();
EnsureTimerTask();
}
}
private static void EnsureUserPresentTask()
{
foreach (var task in BackgroundTaskRegistration.AllTasks)
if (task.Value.Name == TASK_NAME_USERPRESENT)
return;
var builder = new BackgroundTaskBuilder();
builder.Name = TASK_NAME_USERPRESENT;
builder.TaskEntryPoint =(typeof(TileSchedulerTask)).FullName;
builder.SetTrigger(new SystemTrigger(SystemTriggerType.UserPresent, false));
builder.Register();
}
private static void EnsureTimerTask()
{
foreach (var task in BackgroundTaskRegistration.AllTasks)
if (task.Value.Name == TASK_NAME_TIMER)
return;
var builder = new BackgroundTaskBuilder();
builder.Name = TASK_NAME_TIMER;
builder.TaskEntryPoint = (typeof(TileSchedulerTask)).FullName;
builder.SetTrigger(new TimeTrigger(180, false));
builder.Register();
}
- 在
OnNavigatedTo
方法内添加以下代码片段:
_timer.Start();
UpdateClockText();
CreateClockTask();
- 在
MainPage
类定义之后添加以下代码片段:
public sealed class TileSchedulerTask : IBackgroundTask
{
public void Run(IBackgroundTaskInstance taskInstance)
{
var deferral = taskInstance.GetDeferral();
CreateSchedule();
deferral.Complete();
}
public static void CreateSchedule()
{
var tileUpdater = TileUpdateManager.CreateTileUpdaterForApplication();
var plannedUpdated = tileUpdater.GetScheduledTileNotifications();
DateTime now = DateTime.Now;
DateTime planTill = now.AddHours(4);
DateTime updateTime = new DateTime(now.Year, now.Month,now.Day, now.Hour, now.Minute, 0).AddMinutes(1);
if (plannedUpdated.Count > 0)
updateTime = plannedUpdated.Select(x =>x.DeliveryTime.DateTime).Union(new[] { updateTime}).Max();
XmlDocument documentNow = GetTilenotificationXml(now);
tileUpdater.Update(new TileNotification(documentNow) {ExpirationTime = now.AddMinutes(1) });
for (var startPlanning = updateTime;startPlanning < planTill; startPlanning =startPlanning.AddMinutes(1))
{
Debug.WriteLine(startPlanning);
Debug.WriteLine(planTill);
try
{
XmlDocument document = GetTilenotificationXml(startPlanning);
var scheduledNotification = newScheduledTileNotification(document,new DateTimeOffset(startPlanning))
{
ExpirationTime = startPlanning.AddMinutes(1)
};
tileUpdater.AddToSchedule(scheduledNotification);
}
catch (Exception ex)
{
Debug.WriteLine("Error: " + ex.Message);
}
}
}
private static XmlDocument GetTilenotificationXml(DateTime dateTime)
{
string language =GlobalizationPreferences.Languages.First();
var cultureInfo = new CultureInfo(language);
string shortDate = dateTime.ToString(cultureInfo.DateTimeFormat.ShortTimePattern);
string longDate = dateTime.ToString(cultureInfo.DateTimeFormat.LongDatePattern);
var document = XElement.Parse(string.Format(@"<tile>
<visual>
<binding template=""TileSquareText02"">
<text id=""1"">{0}</text>
<text id=""2"">{1}</text>
</binding>
<binding template=""TileWideText01"">
<text id=""1"">{0}</text>
<text id=""2"">{1}</text>
<text id=""3""></text>
<text id=""4""></text>
</binding>
</visual>
</tile>", shortDate, longDate));
return document.ToXmlDocument();
}
}
public static class DocumentExtensions
{
public static XmlDocument ToXmlDocument(thisXElement xDocument)
{
var xmlDocument = new XmlDocument();
xmlDocument.LoadXml(xDocument.ToString());
return xmlDocument;
}
}
- 运行程序。
如何运作...
上述程序显示了如何创建后台基于时间的任务,以及如何在 Windows 8 开始屏幕上的动态磁贴上显示来自此任务的更新。编写 Windows Store 应用程序本身就是一项相当具有挑战性的任务——您必须关心应用程序的暂停/恢复状态等许多其他问题。在这里,我们将专注于我们的主要任务,把次要问题留在后面。
我们的主要目标是在应用程序本身不在前台时运行一些代码。首先,我们创建了IBackgroundTask
接口的实现。这是我们的代码,Run
方法将在收到触发信号时被调用。重要的是,如果Run
方法中包含带有await
的异步代码,我们必须使用一个特殊的延迟对象,如食谱中所示,明确指定我们何时开始和结束Run
方法的执行。在我们的情况下,方法调用是同步的,但为了说明这一要求,我们使用延迟对象。
在我们的任务中,我们在Run
方法中每分钟创建一组磁贴更新,持续 4 小时,并借助ScheduledTaskNotification
类将其注册到TileUpdateManager
中。磁贴使用特殊的 XML 格式来指定文本在其上的位置。当我们从系统触发任务时,它会为接下来的 4 小时安排每分钟的磁贴更新。然后,我们需要注册我们的后台任务。我们进行了两次注册;一次注册提供了UserPresent
触发器,这意味着当用户登录时将触发此任务。下一个触发器是时间触发器,每 3 小时运行一次任务。
当程序运行时,它会创建一个定时器,在应用程序在前台时运行。与此同时,它正在尝试注册后台任务;要注册这些任务,程序需要用户权限,并且将显示一个对话框请求用户的权限。现在我们已经安排了接下来 4 小时的动态磁贴更新。如果我们关闭应用程序,动态磁贴将继续每分钟显示新的时间。在接下来的 3 小时内,时间触发器将再次运行我们的后台任务,并且我们将安排另一个动态磁贴更新。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
2020-05-17 HowToDoInJava Spring 教程·翻译完成