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 控件的交互相比非常方便。