C#图解教程 第二十章 异步编程
笔记
启动一个程序,系统在内存中创建一个新进程,进程内部是系统创建的线程,线程可以派生其他线程,这就有了多线程。
进程内的多个线程共享进程的资源,系统为处理器规划的单元是线程。
异步编程可以实现在新线程里面运行一部分代码,或改变代码的执行顺序。
本章介绍了以下几种异步编程方式,它们居多是并发的而非并行。
async/await
- .NET4.5以上才支持(4.0可以用扩展包支持)
- 简单易用,结合异步方法的控制流,结构清晰明了
- 三种返回模式
- 可以在调用方法中同步/异步地等待任务
- 支持异步Lambda表达式以执行简单程序
- 适合那些需要在后台完成的不相关的小任务
BackgroundWorker
- .NET支持较早
- 在后台持续运行,并不时与主线程通信
- 在WinForm中比较适合在需要与UI层通信时使用
- HelloAsync项目里面的FormBackgroundWorker就是它的示例
Task Parellel
System.Threading.Tasks
中的Parallel.For
和Parallel.ForEach
- 这是真正的多核处理器并行执行程序
BeginInvoke/EndInvoke
- 三种模式
- AsyncResult
- Delegate.Invoke/BeginInvoke vs Control.Invoke/BeginInvoke.md
System.Threading.Timer
异步编程
什么是异步
启动程序时,系统会在内存中创建一个新的进程。进程是构成运行程序的资源集合。包括虚地址空间、文件句柄和许多其他程序运行所需的东西。
在进程内部,系统创建了一个称为线程的内核(kernel)对象,它代表了真正执行的程序。(线程是“执行线程”的简称)一旦进程建立,系统会在Main方法的第一行语句处就开始线程的执行。
关于线程,需要了解以下知识点
- 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束
- 线程可以派生其他线程,因此在任意时刻,一个进程都可能包含不同状态的多个线程,来执行程序的不同部分
- 如果一个进程拥有多个线程,它们将共享进程的资源
- 系统为处理器执行所规划的单元是线程,不是进程
本书目前为止所展示的所有示例程序都只使用了一个线程,并且从程序的第一条语句按顺序执行到最后一条。然而在很多情况下,这种简单的模型都会在性能或用户体验上导致难以接受的行为。
例如,一个服务器程序可能会持续不断地发起到其他服务器的连接,并向它们请求数据,同时处理来自多个客户端程序的请求。这种通信任务往往耗费大量时间,在此期间程序只能等待网络或互联网上其他计算机的响应。这严重削弱了性能。程序不应该浪费等待响应的时间,而应该更加高效,在等待的同时执行其他任务,回复到达后再继续执行第一个任务。
本章我们将学习异步编程。在异步程序中,程序代码不需要按照编写的顺序严格执行。有时需要在一个新的线程中运行一部分代码,有时无需创建新的线程,但为了更好地利用单个线程的能力,需要改变代码的执行顺序。
我们先来看看C#5.0引入的一个用来构建异步方法的新特性——async/await
。接下来学习一些可实现其他形式的异步编程的特性,这些特性是.NET框架的一部分,但没有嵌入C#语言。相关主题包括BackgroundWorker
类和.NET任务并行库。两者均通过新建线程来实现异步。本章最后我们会看看编写异步程序的其他方式。
示例
为了演示和比较,我们先来看一个不使用异步的示例。然后再看一个实现类似功能的异步程序。
在下面的代码示例中,MyDownloadString类的方法DoRun执行以下任务。
- 创建Stopwatch类(位于
System.Diagnostics
命名空间)的一个实例并启动。该Stopwatch计时器用来测量代码中不同任务的执行时间 - 然后两次调用CountCharacters方法,下载某网站的内容,并返问该网站包含的字符数。网站由URL字符串指定,作为第二个参数传入
- 接着四次调用CountToALargeNumber方法。该方法仅执行一个消耗一定时间的任务,并循环指定次数
- 最后,打印两个网站的字符数
using System; using System.Net; using System.Diagnostics; class MyDownloadString { Stopwatch sw = new Stopwatch(); public void DoRun() { const int LargeNumber = 6000000; sw.Start(); int t1 = CountCharacters(1, "http://www.microsoft.com"); int t2 = CountCharacters(2, "http://www.illustratedcsharp.com"); CountToALargeNumber(1, LargeNumber); CountToALargeNumber(2, LargeNumber); CountToALargeNumber(3, LargeNumber); CountToALargeNumber(4, LargeNumber); Console.WriteLine("Chars in http://www.microsoft.coin :{0}", t1); Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2); } private int CountCharacters(int id, string uriString) { WebClient wc1 = new WebClient(); Console.WriteLine("Starting call {0} : {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds); string result = wc1.DownloadString(new Uri(uriString)); Console.WriteLine(" Call {0} completed: {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds); return result.Length; } private void CountToALargeNumber(int id, int value) { for (long i = 0; i < value; i++) ; Console.WriteLine(" End counting {0} : {1,4:N} ms", id, sw.Elapsed.TotalMilliseconds); } } class Program { static void Main() { MyDownloadString ds = new MyDownloadString(); ds.DoRun(); Console.ReadKey(); } }
输出:
Starting call 1 : 0.39 ms Call 1 completed: 131.95 ms Starting call 2 : 132.04 ms Call 2 completed: 655.97 ms End counting 1 : 670.80 ms End counting 2 : 685.57 ms End counting 3 : 700.00 ms End counting 4 : 714.46 ms Chars in http://www.microsoft.coin :1020 Chars in http://www.illustratedcsharp.com: 210
下图总结了输出结果,展示了不同任务开始和结束的时间。如图所示,Call1和Call2占用了大部分时间。但不管哪次调用,绝大部分时间都浪费在等待网站的响应上。
如果我们能初始化两个CountCharacter调用,无需等待结果,而是直接执行4个CountToALargeNumber调用,然后在两个CountCharacter方法调用结束时再获取结果,就可以显著地提升性能。
C#最新的async/await
特性就允许我们这么做。可以重写代码以运用该特性,如下所示。稍后我会深入剖析这个特性,现在先来看看本示例需要注意的几个方面。
- 当DoRun调用CountCharactersAsync时,CountCharactersAsync将立即返回,然后才真正开始下载字符。它向调用方法返回的是一个
Task<int>
类型的占位符对象,表示它计划进行的工作。这个占位符最终将“返回”一个int - 这使得DoRun不用等待实际工作完成就可继续执行。下一条语句是再次调用 CountCharactersAsync,同样会返回一个
Task<int>
对象 - 接着,DoRun可以继续执行,调用4次 CountToALargeNumber,同时 CountCharactersAsync 的两次调用继续它们的工作——基本上是等待(网站的响应)
- DoRun 的最后两行从 CountCharactersAsync 调用返回的
Tasks
中获取结果。如果还没有结果,将阻塞并等待
using System; using System.Diagnostics; using System.Net; using System.Threading.Tasks; class MyDownloadString { Stopwatch sw = new Stopwatch(); public void DoRun() { const int LargeNumber = 6000000; sw.Start(); Task<int> t1 = CountCharactersAsync(1, "http://www.microsoft.com"); Task<int> t2 = CountCharactersAsync(2, "http://www.illustratedcsharp.com"); CountToALargeNumber(1, LargeNumber); CountToALargeNumber(2, LargeNumber); CountToALargeNumber(3, LargeNumber); CountToALargeNumber(4, LargeNumber); Console.WriteLine("Chars in http://www.microsoft.coin :{0}", t1.Result); Console.WriteLine("Chars in http://www.illustratedcsharp.com: {0}", t2.Result); } private async Task<int> CountCharactersAsync(int id, string site) { WebClient wc = new WebClient(); Console.WriteLine("Starting call {0} : {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds); string result = await wc.DownloadStringTaskAsync(new Uri(site)); Console.WriteLine(" Call {0} completed : {1, 4:N} ms", id, sw.Elapsed.TotalMilliseconds); return result.Length; } private void CountToALargeNumber(int id, int value) { for (long i = 0; i < value; i++) ; Console.WriteLine(" End counting {0} : {1,4:N} ms", id, sw.Elapsed.TotalMilliseconds); } } class Program { static void Main() { MyDownloadString ds = new MyDownloadString(); ds.DoRun(); Console.ReadKey(); } }
输出:
Starting call 1 : 1.33 ms Starting call 2 : 66.50 ms End counting 1 : 83.81 ms End counting 2 : 124.33 ms Call 1 completed : 124.35 ms End counting 3 : 138.55 ms End counting 4 : 152.52 ms Chars in http://www.microsoft.coin :1020 Call 2 completed : 623.79 ms Chars in http://www.illustratedcsharp.com: 210
下图总结了输出结果,展示了修改后的程序的时间轴。新版程序比旧版快了32%。这是由于 CountToALargeNumber 的4次调用是在 CountCharactersAsync 方法调用等待网站响应的时候进行的。所有这些工作都是在主线程中完成的,我们没有创建任何额外的线程!
async/await特性的结构
我们已经看到了一个异步方法的示例,现在来讨论其定义和细节。
如果一个程序调用某个方法,等待其执行所有处理后才继续执行,我们就称这样的方法是同步的。这是默认形式,在本章之前你所看到的都是这种形式。
相反,异步的方法在处理完成之前就返回到调用方法。C#的async/await
特性可以创建并使用异步方法。该特性由三个部分组成,如下所示。
- 调用方法(calling method):该方法调用异步方法,然后在异步方法(可能在相同的线程,也可能在不同的线程)执行其任务的时候继续执行
- 异步(async)方法:该方法异步执行其工作,然后立即返回到调用方法
- await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,不过如果一个都不包含的话编译器会发出警告
Class Program { static void Main() { ... //调用方法 Task<int> value=DoAsyncStuff.CalculateSumAsync(5,6); ... } } static class DoAsyncStuff { //异步方法 public static async Task<int> CalculateSumAsync(int i1,int i2) { //await表达式 int sum=await TaskEx.Run(()=>GetSum(i1,i2)); return sum; } ... }
什么是异步方法
如上节所述,异步方法在完成其工作之前即返回到调用方法,然后在调用方法继续执行的时候完成其工作。
在语法上,异步方法具有如下特点,如下图。
- 方法头中包含
async
方法修饰符 - 包含一个或多个
await
表达式,表示可以异步完成的任务。 - 必须具备以下三种返回类型。第二种(
Task
)和第三种(Task<T>
)的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行void
Task
Task<T>
- 异步方法的参数可以为任意类型任意数量,但不能为out或ref参数
- 按照约定,异步方法的名称应该以Async为后缀
- 除了方法以外,Lambda表达式和匿名方法也可以作为异步对象。
关键字 返回类型 ↓ ↓ async Task<int> CountCharactersAsync(int id,string site) { WebClient wc = new WebClient(); Console.WriteLine( "Starting call {0} : {1, 4:N} ms",id, sw.Elapsed.TotalMilliseconds); // await表达式 string result = await wc.DownloadStringTaskAsync( new Uri(site)); Console.WriteLine( " Call {0} completed: {1, 4:N} ms",id, sw.Elapsed.TotalMilliseconds); // 返回语句 return result.Length; }
上例阐明了一个异步方法的组成部分,现在我们可以详细介绍了。
第一项是async
关键字。
- 异步方法在方法头中必须包含
async
关键字,且必须出现在返回类型之前 - 该修饰符只是标识该方法包含一个或多个
await
表达式。也就是说,它本身并不能创建任何异步操作。 async
关键字是一个上下文关键字,也就是说除了作为方法修饰符(或Lambda表达式修饰符、匿名方法修饰符)之外,async
还可用作标识符
返回类型必须是以下三种类型之一。注意,其中两种都涉及Task类。我在指明类的时候,将使用大写形式(类名)和语法字体来区分。在表示一系列需要完成的工作时,将使用小写字母和一般字体。
Task<T>
:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task<T>
。调用方法将通过读取Task的Result属性来获取这个T类型的值。下面的代码来自一个调用方法,阐明了这一点:
Task<int> value = DoStuff.CalculateSumAsync(5,6); ... Console,WriteLine( "Value: {0}", value.Result);
Task
:如果调用方法不需要从异步方法中返回某个值,但需要检査异步方法的状态,那么异步方法可以返回一个Task类型的对象。这时,即使异步方法中出现了 return语句,也不会返回任何东西。下面的代码同样来自调用方法:
Task someTask = DoStuff.CalculateSumAsync(5,6); ... someTask.Wait();
void
:如果调用方法仅仅想执行异步方法,而不需要与它做任何进一步的交互时[这称为调用并忘记(fire and forget)],异步方法可以返回void类型。这时,与上一种情况类似,即使异步方法中包含任何return语句,也不会返回任何东西
注意上例中异步方法的返回类型为Task<int>
。但方法体中不包含任何返回Task<int>
类型对象的return语句。相反,方法最后的return语句返回了一个int类型(result.Length)的值。我们先将这一发现总结如下,稍后再详细解释。
- 任何返回
Task<T>
类型的异步方法其返回值必须为T类型或可以隐式转换为T的类型
下面阐明了调用方法和异步方法在用这三种返回类型进行交互时所需的体系结构。
使用返回Task<int>
对象的异步方法
using System; using System.Threading.Tasks; class Program { static void Main() { Task<int> value=DoAsyncStuff.CalculateSumAsync(5,6); //处理其他事情 Console.WriteLine("Value: {0}",value.Result); } } static class DoAsyncStuff { public static async Task<int> CalculateSumAsync(int i1,int i2) { int sum=await Task.Run(()=>GetSum(i1,i2)); return sum; } private static int GetSum(int i1,int i2) { return i1+i2; } }
使用返回Task
对象的异步方法
using System; using System.Threading.Tasks; class Program { static void Main() { Task someTask=DoAsyncStuff.CalculateSumAsync(5,6); //处理其他事情 someTask.Wait(); Console.WriteLine("Async stuff is done"); } } static class DoAsyncStuff { public static async Task CalculateSumAsync(int i1,int i2) { int value=await Task.Run(()=>GetSum(i1,i2)); Console.WriteLine("Value: {0}",value); } private static int GetSum(int i1,int i2) { return i1+i2; } }
输出:
Value: 11 Async stuff is done
下例中使用Thread.Sleep
方法来暂停当前线程,所以异步方法完成时,它还没有完成。
使用“调用并忘记”的异步方法
using System; using System.Threading; using System.Threading.Tasks; class Program { static void Main() { DoAsyncStuff.CalculateSumAsync(5,6); //处理其他事情 Thread.Sleep(200); Console.WriteLine("Program Exiting"); } } static class DoAsyncStuff { public static async void CalculateSumAsync(int i1,int i2) { int value=await Task.Run(()=>GetSum(i1,i2)); Console.WriteLine("Value: {0}",value); } private static int GetSum(int i1,int i2) { return i1+i2; } }
输出:
Value: 11 Program Exiting
异步方法的控制流
异步方法的结构包含三个不同的区域,如下图所示。我将在下节详细介绍await表达式,不过在本节你将对其位置和作用有个大致了解。这三个区域如下:
- 第一个await表达式之前的部分:从方法开头到第一个await表达式之间的所有代码。这一部分应该只包含少量且无需长时间处理的代码
- await表达式:表示将被异步执行的任务
- 后续部分:在await表达式之后出现的方法中的其余代码。包括其执行环境,如所在线程信息、目前作用域内的变量值,以及当await表达式完成后要重新执行所需的其他信息
下图阐明了一个异步方法的控制流。它从第一个await表达式之前的代码开始,正常执行 (同步地)直到遇见第一个await。这一区域实际上在第一个await表达式处结束,此时await任务还没有完成(大多数情况下如此)。当await任务完成时,方法将继续同步执行。如果还有其他await,就重复上述过程。
当达到await表达式时,异步方法将控制返回到调用方法。如果方法的返回类型为Task
或Task<T>
类型,将创建一个Task
对象,表示需异步完成的任务和后续,然后将该Task
返回到调用方法。
目前有两个控制流:异步方法内的和调用方法内的。异步方法内的代码完成以下工作。
- 异步执行await表达式的空闲任务
- 当await表达式完成时,执行后续部分。后续部分本身也可能包含其他await表达式,这些表达式也将按照相同的方式处理,即异步执行await表达式,然后执行后续部分
- 当后续部分遇到return语句或到达方法末尾时,将:
- 如果方法返回类型为
void
,控制流将退出 - 如果方法返冋类型为
Task
,后续部分设置Task
的属性并退出。如果返回类型为Task<T>
,后续部分还将设置Task
对象的 Result 属性
- 如果方法返回类型为
同时,调用方法中的代码将继续其进程,从异步方法获取Task
对象。当需要其实际值时,就引用Task
对象的 Result 属性。届时,如果异步方法设置了该属性,调用方法就能获得该值并继续。否则,将暂停并等待该属性被设置,然后再继续执行。
很多人可能不解的一点是同步方法第一次遇到await时所返回对象的类型。这个返回类型就是同步方法头中的返回类型,它与await表达式的返回值类型一点关系也没有。
例如下面的代码,await表达式返回一个string。但在方法的执行过程中,当到达await表达式时,异步方法返回到调用方法的是一个Task<int>
对象,这正是该方法的返回类型。
private async Task<int> CountCharactersAsync(string site) { WebClient wc=new WebClient(); string result=await wc.DownloadStringTaskAsync(new Uri(site)); return result.Length; }
另一个可能让人迷惑的地方是,异步方法的return语句“返回”一个结果或到达异步方法末尾时,它并没有真正地返回某个值——它只是退出了。
await表达式
await表达式指定了一个异步执行的任务。其语法如下所示,由await关键字和一个空闲对象 (称为任务)组成。这个任务可能是一个Task
类型的对象,也可能不是。默认情况下,这个任务在当前线程异步运行。
await task
一个空闲对象即是一个awaitable
类型的实例。awaitable
类型是指包含GetAwaiter
方法的类型,该方法没有参数,返回一个称为awaiter
类型的对象。awaiter
类型包含以下成员:
bool IsCompleted{get;}
void OnCompleted(Action);
它还包含以下成员之一:
void GetResult();
T GetResult();//T为任意类型
然而实除上,你并不需要构建自己的awaitable
。相反,你应该使用Task
类,它是awaitable
类型。对于awaitable
,大多数程序员所需要的就是Task
了。
在.NET4.5中,微软发布了大量新的和修订的异步方法(在BCL中),它们可返回Task<T>
类型的对象。将这些放到你的await表达式中,它们将在当前线程中异步执行。
在之前的很多示例中,我们都使用了WebClient.DownloadStringTaskAsync
方法,它也是这些异步方法中一个。以下代码阐明了其用法:
Uri site = new Uri("http://www.illustratedcsharp.com"); WebClient wc = new WebClient(); string result = await wc.DownloadStringTaskAsync(site);
尽管目前BCL中存在很多返回Task<T>
类型对象的方法,你仍然可能需要编写自己的方法, 作为await表达式的任务。最简单的方式是在你的方法中使用Task.Run
方法来创建一个Task
。关于Task.Run
,有一点非常重要,即它是在不同的线程上运行你的方法。
Task.Run
的一个签名如下,以Func<TReturn>
委托(Delegate)为参数。如第19章所述,Func<TReturn>
是一个预定义的委托,它不包含任何参数,返回值的类型为TReturn
:
Task Run(Func<TReturn> func)
因此,要将你的方法传递给Task.Run
方法,需要基于该方法创建一个委托。下面的代码展示了三种实现方式。其中,Get10与Func<int>
委托兼容,因为它没有参数并且返回int。
- 第一个实例(DoWorkAsync方法的前两行)使用Get10创建名为ten的
Func<int>
委托。然后在下一行将该委托用于Task.Run方法 - 第二个实例在了Task.Run方法的参数列表中创建
Func<int>
委托 - 第三个实例没有使用Get10方法。而是使用了组成Get10方法的return语句,将其用于与
Func<int>
委托兼容的Lambda表达式。该Lambda表达式将隐式转换为该委托
class MyClass { public int Get10() { return 10; } public async Task DoWorkAsync() { // 单独创建 Func<TReturn> 委托 Func<int> ten=new Func<int>(Get10); int a=await Task.Run(ten); // 参数列表中创建 Func<TReturn> 委托 int b=await Task.Run(new Func<int>(Get10)); // 隐式转换为 Func<TReturn> 委托的 Lambda表达式 int c=await Task.Run(()=>{return 10;}); Console.WriteLine("{0} {1} {2}",a,b,c); } } class Program { static void Main() { Task t=(new MyClass()).DoWorkAsync(); t.Wait(); } }
输出:
10 10 10
在上面的示例代码中,我们使用的Task.Run的签名以Func<TResult>
为参数。该方法共有8个重载,如下表所示。
下表展示了可能用到的4个委托类型的签名。
下面的代码展示了4个await语句,使用Task.Run
方法来运行4种不同的委托类型所表示的方法:
static class MyClass { public static async Task DoWorkAsync() { Action ↓ await Task.Run(() => Console.WriteLine(5.ToString())); TResult Func() ↓ Console.WriteLine((await Task.Run(() => 6)).ToString()); Task Func() ↓ await Task.Run(() => Task.Run(() => Console.WriteLine(7.ToString()))); Task<TResult> Func() ↓ int value = await Task.Run(() => Task.Run(() => 8)); Console.WriteLine(value.ToString()); } } class Program { static void Main() { Task t = MyClass.DoWorkAsync(); t.Wait(); Console.WriteLine("Press Enter key to exit"); Console.Read(); } }
输出:
5 6 7 8 Press Enter key to exit
在能使用任何其他表达式的地方,都可以使用await表达式(只要位于异步方法内)。在上面的代码中,4个await表达式用在了3个不同的位置。
- 第一个和第三个实例将await表达式用作语句。
- 第二个实例将await表达式用作WriteLine方法的参数。
- 第四个实例将await表达式用作赋值语句的右端。
假设我们的某个方法不符合这4种委托形式。例如,假设有一个GetSum
方法以两个int值作为输入,并返回这两个值的和。这与上述4个可接受的委托都不兼容。要解决这个问题,可以用可接受的Func
委托的形式创建一个Lambda函数,其唯一的行为就是运行GetSum
方法,如下面的代码所示:
int value = await Task.Run(()=> GetSum(5,6));
Lambda函数()=>GetSum(5,6)
满足Func<TResult>
委托,因为它没有参数,且返回单一的值。
下面的代码展示了完整的示例:
static class MyClass { private static int GetSum(int i1, int i2) { return i1+i2; } public static async Task DoWorkAsync() { int value=await Task.Run(()=>GetSum(5,6)); Console.WriteLine(value.ToString()); } } class Program { static void Main() { Task t = MyClass.DoWorkAsync(); t.Wait(); Console.WriteLine("Press Enter key to exit"); Console.Read(); } }
输出:
11 Press Enter key to exit
取消一个异步操作
一些.NET异步方法允许你请求终止执行。你同样也可以在自己的异步方法中加入这个特性。
System.Threading.Tasks
命名空间中有两个类是为此目的而设计的:CancellationToken
和CancellationTokenSource
。
CancellationToken
对象包含一个任务是否应被取消的信息- 拥有
CancellationToken
对象的任务需要定期检查其令牌(token)状态。如果CancellationToken
对象的IsCancellationRequested
属性为true
,任务需停止其操作并返回 CancellationToken
是不可逆的,并且只能使用一次。也就是说,一旦IsCancellationRequested
属性被设置为true
,就不能更改了CancellationTokenSource
对象创建可分配给不同任务的CancellationToken
对象。任何持有CancellationTokenSource
的对象都可以调用其Cancel
方法,这会将CancellationToken
的IsCancellationRequested
属性设置为true
下面的代码展示了如何使用CancellationTokenSource
和CancellationToken
来实现取消操作。注意,该过程是协同的。即调用CancellationTokenSource
的Cancel
时,它本身并不会执行取消操作。而是会将CancellationToken
的IsCancellationRequested
属件设置为true
。包含CancellationToken
的代码负责检查该属性,并判断是否需要停止执行并返回。
下面的代码展示了如何使用这两个取消类。如下所示代码并没有取消异步方法,而是在Main方法中间有两行被注释的代码,它们触发了取消行为。
using System; using System.Threading; using System.Threading.Tasks; class Program { static void Main() { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token; MyClass mc = new MyClass(); Task t = mc.RunAsync(token); //Thread.Sleep(3000);//等待3秒 //cts.Cancel(); //取消操作 t.Wait(); Console.WriteLine("Was Cancelled: {0}", token.IsCancellationRequested); Console.ReadKey(); } } class MyClass { public async Task RunAsync(CancellationToken ct) { if (ct.IsCancellationRequested) return; await Task.Run(() => CycleMethod(ct), ct); } // CycleMethod彻底执行完需要5s void CycleMethod(CancellationToken ct) { Console.WriteLine("Starting CycleMethod"); const int max = 5; for (int i = 0; i < max; i++) { if (ct.IsCancellationRequested) // 监控CancellationToken return; Thread.Sleep(1000); Console.WriteLine(" {0} of {1} iterations completed", i + 1, max); } } }
第一次运行时保留注释的代码,不会取消任务,产生的结果如下:
Starting CycleMethod 1 of 5 iterations completed 2 of 5 iterations completed 3 of 5 iterations completed 4 of 5 iterations completed 5 of 5 iterations completed Was Cancelled: False
如果取消Main方法中对Thread.Sleep
和Cancel
语句的屏蔽,任务将在3秒后取消,产生的结果如下:
Starting CycleMethod 1 of 5 iterations completed 2 of 5 iterations completed 3 of 5 iterations completed Was Cancelled: True
异常处理和await表达式
可以像使用其他表达式那样,将await表达式放在try语句内,try…catch…finally结构将按你期望的那样工作。
下面的代码展示了一个示例,其中await表达式中的任务会抛出一个异常。await表达式位于try块中,将按普通的方式处理异常。
class Program { static void Main(string[] args) { Task t = BadAsync(); t.Wait(); Console.WriteLine("Task Status : {0}", t.Status); Console.WriteLine("Task IsFaulted: {0}", t.IsFaulted); } static async Task BadAsync() { try { await Task.Run(() => { throw new Exception(); }); } catch { Console.WriteLine("Exception in BadAsync"); } } }
输出:
Exception in BadAsync Task Status : RanToCompletion Task IsFaulted: False
注意,尽管Task抛出了一个Exception,在Main的最后,Task的状态仍然为RanToCompletion。这会让人感到很意外,因为异步方法抛出了异常。
原因是以下两个条件成立:(1)Task没有被取消,(2)没有未处理的异常。类似地,IsFaulted
属性为False
,因为没有未处理的异常。
在调用方法中同步地等待任务
调用方法可以调用任意多个异步方法并接收它们返回的Task对象。然后你的代码会继续执行其他任务,但在某个点上可能会需要等待某个特殊Task对象完成,然后再继续。为此,Task
类提供了一个实例方法Wait
,可以在Task对象上调用该方法。
下面的示例展示了其用法。在代码中,调用方法DoRun
调用异步方法CountCharactersAsync
并接收其返回的Task<int>
。然后调用Task
实例的Wait
方法,等待任务Task结束。等结束时再显示结果信息。
static class MyDownloadString { public static void DoRun() { Task<int> t = CountCharactersAsync("https://www.zhihu.com/"); //t.Wait(); Console.WriteLine("The task is executing."); Console.WriteLine("The task has finished, returning value {0}.", t.Result); } private static async Task<int> CountCharactersAsync(string site) { string result = await new WebClient().DownloadStringTaskAsync(new Uri(site)); return result.Length; } } class Program { static void Main() { MyDownloadString.DoRun(); Console.ReadKey(); } }
输出:
The task is executing. The task has finished, returning value 8328.
屏蔽t.Wait();
时,先输出第一句,Task<int> t
执行完成后输出第二句;不屏蔽t.Wait();
时,Task<int> t
执行完成后同时输出这两句。
Wait
方法用于单一Task对象。而你也可以等待一组Task对象。对于一组Task,可以等待所有任务都结束,也可以等待某一个任务结束。实现这两个功能的是Task
类中的两个静态方法:
WaitAll
WaitAny
这两个方法是同步方法且没有返回值。它们停止,直到条件满足后再继续执行。
我们来看一个简单的程序,它包含一个DoRun
方法,两次调用一个异步方法并获取其返回的两个Task<int>
对象。然后,方法继续执行,检査任务是否完成并打印。
如下所示的程序并没有使用等待方法,而是在DoRun
方法中间注释的部分包含等待的代码,我们将在稍后用它来与现在的版本进行比较。
class MyDownloadString { Stopwatch sw = new Stopwatch(); public void DoRun() { sw.Start(); Task<int> t1 = CountCharactersAsync( 1, "http://www.microsoft.com"); Task<int> t2 = CountCharactersAsync( 2, "http://www.illustratedcsharp.com" ); //Task<int>[] tasks = new Task<int>[]{ t1, t2 }; //Task.WaitAll( tasks ); //Task.WaitAny( tasks ); Console.WriteLine( "Task 1: {0}Finished", t1.IsCompleted ? "" : "Not "); Console.WriteLine( "Task 2: {0}Finished", t2.IsCompleted ? "" : "Not "); Console.Read(); } private async Task<int> CountCharactersAsync( int id, string site ) { WebClient wc = new WebClient(); string result = await wc.DownloadStringTaskAsync( new Uri( site )); Console.WriteLine(" Call {0} completed: {1} ms",id, sw.Elapsed.TotalMilliseconds ); return result.Length; } } class Program { static void Main() { MyDownloadString ds = new MyDownloadString(); ds.DoRun(); } }
代码产生的结果如下。注意,在检査这两个Task
的IsCompleted
方法时,没有一个是完成的。
Task 1: Not Finished Task 2: Not Finished Call 1 completed: 127.3862 ms Call 2 completed: 647.7455 ms
如果我们取消DoRun
中间那两行代码中第一行的注释(如下面的三行代码所示),方法将创建一个包含这两个任务的数组,并将这个数组传递给WaitAll
方法。这时代码会停止并等待任务全部完成,然后继续执行。
Task<int>[] tasks = new Task<int>[] {t1,t2}; Task.WaitAll( tasks ); //Task.WaitAny( tasks );
此时运行代码,其结果如下:
Call 1 completed: 100.8518 ms Call 2 completed: 551.1589 ms Task 1: Finished Task 2: Finished
如果我们再次修改代码,注释掉WaitAll
方法调用,取消WaitAny
方法调用的注释,代码将如下所示:
Task<int>[] tasks = new Task<int>[] {t1,t2}; //Task.WaitAll( tasks ); Task.WaitAny( tasks );
这时,WaitAny
调用将终止并等待至少一个任务完成。运行代码的结果如下:
Call 1 completed: 158.7846 ms Task 1: Finished Task 2: Not Finished Call 2 completed: 610.8676 ms
WaitAll
和WaitAny
分别还包含4个重载,除了任务完成之外,还允许其他继续执行的方式,如设置超时时间或使用CancellationToken
来强制执行处理的后续部分。下表展示了这些重载方法。
在异步方法中异步地等待任务
上节学习了如何同步地等待Task完成。但有时在异步方法中,你会希望用await表达式来等待Task。这时异步方法会返回到调用方法,但该异步方法会等待一个或所有任务完成。可以通过Task.WhenAll
和Task.WhenAny
方法来实现。这两个方法称为组合子(combinator)。
下面的代码展示了一个使用Task.WhenAll
方法的示例。它异步地等待所有与之相关的Task完成,不会占用主线程的时间。注意,await表达式的任务就是调用Task.WhenAll
。
using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; class MyDownloadString { public void DoRun() { Task<int> t = CountCharactersAsync( "http://www.microsoft.com", "http://www.illustratedcsharp.com"); Console.WriteLine( "DoRun: Task {0}Finished", t.IsCompleted ? "": "Not " ); Console.WriteLine( "DoRun: Result = {0}", t.Result ); } private async Task<int> CountCharactersAsync(string sitel, string site2) { WebClient wcl = new WebClient(); WebClient wc2 = new WebClient(); Task<string> t1 = wcl.DownloadStringTaskAsync( new Uri( sitel )); Task<string> t2 = wc2.DownloadStringTaskAsync( new Uri( site2 )); List<Task<string>> tasks = new List<Task<string>>(); tasks.Add( t1 ); tasks.Add( t2 ); await Task.WhenAll( tasks ); Console.WriteLine(" CCA: T1 {0}Finished", t1.IsCompleted ? "" : "Not "); Console.WriteLine(" CCA: T2 {0}Finished", t2.IsCompleted ? "" : "Not "); return t1.IsCompleted ? t1.Result.Length : t2.Result.Length; } } class Program { static void Main() { var ds=new MyDownloadString(); ds.DoRun(); } }
输出:
DoRun: Task Not Finished CCA: T1 Finished CCA: T2 Finished DoRun: Result = 1020
Task.WhenAny
组合子会异步地等待与之相关的某个Task完成。如果将上面的await表达式由调用Task.WhenAll
改为调用Task.WhenAny
,并返回到程序,将产生以下输出结果:
DoRun: Task Not Finished CCA: T1 Finished CCA: T2 Not Finished DoRun: Result = 1020
Task.Delay方法
Task.Delay
方法创建一个Task对象,该对象将暂停其在线程中的处理,并在一定时间之后完成。然而与Thread.Sleep
阻塞线程不同的是,Task.Delay
不会阻塞线程,线程可以继续处理其他工作。
下面的代码展示了如何使用Task.Delay
方法:
class Simple { Stopwatch sw = new Stopwatch(); public void DoRun() { Console.WriteLine( "Caller: Before call"); ShowDelayAsync(); Console.WriteLine( "Caller: After call"); } private async void ShowDelayAsync() { sw.Start(); Console.WriteLine( " Before Delay: {0}", sw.ElapsedMilliseconds ); await Task.Delay( 1000 ); Console.WriteLine( " After Delay : {0}", sw.ElapsedMilliseconds ); } } class Program { static void Main() { var ds = new Simple (); ds.DoRun(); Console.Read(); } }
输出:
Caller: Before call Before Delay: 0 Caller: After call After Delay : 1013
Delay
方法包含4个重载,可以以不同方式来指定时间周期,同时还允许使用CancellationToken
对象。下表展示了该方法的4个重载。
在GUI程序中执行异步操作
尽管本章目前的所有代码均为控制台应用程序,但实际上异步方法在GUI程序中尤为有用。
原因是GUI程序在设计上就要求所有的显示变化都必须在主GUI线程中完成,如点击按钮、展示标签、移动窗体等。Windows程序是通过消息来实现这一点的,消息被放入由消息泵管理的消息队列中。
消息泵从队列中取出一条消息,并调用它的处理程序(handler)代码。当处理程序代码完成时,消息泵获取下一条消息并循环这个过程。
由于这种架构,处理程序代码就必须快捷,这样才不至于挂起并阻碍其他GUI行为的处理。如果某个消息的处理程序代码耗时过长,消息队列中的消息会产生积压。程序将失去响应,因为在那个长时间运行的处理程序完成之前,无法处理任何消息。
下图展示了一个WPF程序中两个版本的窗体。窗体由状态标签及其下方的按钮组成。开发者的目的是,程序用户点击按钮,按钮的处理程序代码执行以下操作:
- 禁用按钮,这样在处理程序执行期间用户就不能再次点击了
- 将标签文本改为Doing Stuff,这样用户就会知道程序正在工作
- 让程序休眠4秒钟——模拟某个工作
- 将标签文本改为原始文本,并启用按钮。
右图的截屏展示了开发者希望在按钮按下的4秒之内窗体的样子。然而事实并非如此。当开发者点击按钮后,什么都没有发生。而且如果在点击按钮后移动窗体,会发现它已经冻结,不会移动——直到4秒之后,窗体才突然出现在新位置。
注意 WPF是微软替代Windows Form的GUI编程框架。要了解更多关于WPF编程的知识,请参阅笔者的 Illustrated WPF(Apress,2009)一书。
要使用Visual Studio 2012创建这个名为MessagePump的WPF程序,步骤如下:
1.选择File→New→Project菜单项,弹出New Project窗口
2.在窗口左侧的面板内,展开Installed Templates(如果没有展开的话)
3.在C#类别中点击Windows条目,将在中间面板中弹出已安装的Windows程序模板
4.点击WPF Application,在窗口下方的Name文本框中输人MessagePump。在其下方选择一个位置,并点击OK按钮
5.将MainWindow.xaml中的XAML标记修改为下面的代码,在窗体中创建状态标签和按钮。
<Window x:Class="MessagePump.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Pump" Height="120" Width="200"> <StackPanel> <Label Name="lblStatus" Margin="10,5,10,0" >Not Doing Anything</Label> <Button Name="btnDoStuff" Content="Do Stuff" HorizontalAlignment="Left" Margin="10,5" Padding="5,2" Click="btnDoStuff_Click"/> </StackPanel> </Window>
6.将代码隐藏文件MainWindow.xaml.cs修改为如下C#代码。
using System.Threading; using System.Threading.Tasks; using System.Windows; namespace MessagePump { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void btnDoStuff_Click( object sender, RoutedEventArgs e ) { btnDoStuff.IsEnabled = false; lblStatus.Content = "Doing Stuff"; Thread.Sleep( 4000 ); lblStatus.Content = "Not Doing Anything"; btnDoStuff.IsEnabled = true; } } }
运行程序,你会发现其行为与之前的描述完全一致,即按钮没有禁用,状态标签也没有改变,在4秒之内窗体也无法移动。
这个奇怪行为的原因其实非常简单。下图展示了这种情形。点击按钮时,按钮的Click消息放入消息队列。消息泵从队列中移除该消息并开始处理点击按钮的处理程序代码,即btnDoStuff_Click
方法。btnDoStuff_Click
处理程序将我们希望触发的行为的消息放入队列,如下右图所示。但在处理程序本身退出(即休眠4秒并退出)之前,这些消息都无法执行。然后所有的行为都发生了,但速度太快肉眼根本看不见。
但是,如果处理程序能将前两条消息压入队列,然后将自己从处理器上摘下,在4秒之后再将自己压入队列,那么这些以及所有其他消息都可以在等待的时间内被处理,整个过程就会如我们之前预料的那样,并且还能保持响应。
我们可以使用async/await
特性轻松地实现这一点,如下面修改的处理程序代码。当到达await
语句时,处理程序返回到调用方法,并从处理器上摘下。这时其他消息得以处理——包括处理程序已经压入队列的那两条。在空闲任务完成后(本例中为Task.Delay
),后续部分(方法剩余部分)又被重新安排到线程上。
private async void btnDoStuff_Click(object sender, RoutedEventArgs e ) { btnDoStuff.IsEnabled = false; lblStatus.Content = "Doing Stuff"; await Task.Delay( 4000 ); lblStatus.Content = "Not Doing Anything"; btnDoStuff.IsEnabled = true; }
Task.Yield
Task.Yield
方法创建一个立即返回的awaitable。等待一个Yield
可以让异步方法在执行后续部分的同时返回到调用方法。可以将其理解成离开当前的消息队列,回到队列末尾,让处理器有时间处理其他任务。
下面的示例代码展示了一个异步方法,程序每执行某个循环1000次就移交一次控制权。每次执行Yield
方法,都会允许线程中的其他任务得以执行。
static class DoStuff { public static async Task<int> FindSeriesSuw( int il ) { int sum = 0; for ( int i=0; i < il; i++ ) { sum += i; if ( i % 1000 == 0 ) await Task.Yield(); } return sum; } } class Program { static void Main() { Task<int> value = DoStuff.FindSeriesSuw( 1000000 ); CountBig( 100000 ); CountBig( 100000 ); CountBig( 100000 ); CountBig( 100000 ); Console.WriteLine( "Sum: {0}", value.Result ); Console.ReadKey(); } private static void CountBig( int p ) { for ( int i=0; i < p; i++) ; } }
输出:
Sum: 1783293664
Yield
方法在GUI程序中非常有用,可以中断大量工作,让其他任务使用处理器。
使用异步Lambda表达式
到目前为止,本章只介绍了异步方法。但我曾经说过,你还可以使用异步匿名方法和异步Lambda表达式。这种构造尤其适合那些只有很少工作的事件处理程序。下面的代码片段将一个Lambda表达式注册为一个按钮点击事件的事件处理程序。
startWorkButton.Click += async (sender,e )=> { //处理点击处理程序工作 }
下面用一个简短的WPF程序来展示其用法,下面为后台代码:
using System.Threading.Tasks; using System.Windows; namespace AsyncLambda { public partial class MainMindow : Window { public MainMindow() { InitializdComponent(); startWorkButton.Click += async (sender,e)=> { SetGuiValues( false, "Work Started"); await DoSomeWork(); SetGuiValues( true, "Work Finished"); }; } private void SetGuiValues(bool buttonEnabled, string status) { startWorkButton.IsEnabled = buttonEnabled; workStartedTextBlock.Text = status; } private Task DoSomeWork() { return Task.Delay(2500); } } }
XAML文件中的标记如下:
<Window x:Class="AsyncLambda.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Async Lambda" Height="115" Width="150"> <StackPanel> <TextBlock Name="workStartedTextBlock" Margin="10,10"/> <Button Name="startWorkButton" Content="Start Work" Width="100" Margin="4"/> </StackPanel> </Window>
完整的GUI程序
我们循序渐进地介绍了async/await
组件。本节你将看到一个完整的WPF GUI程序,包含一个状态条和取消操作。
如下图所示,左边为示例程序的截图。点击按钮,程序将开始处理并更新进度条。处理过程完成将显示右上角的消息框。如果在处理完成前点击Cancel按钮,程序将显示右下角的消息框。
我们首先创建一个名为WpfAwait的WPF应用程序。按如下的代码修改MainWindow.xaml中的XAML标记:
<Window x:Class="WpfAwait.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Process and Cancel" Height="150" Width="250"> <StackPanel> <Button Name="btnProcess" Width="100" Click="btnProcess_Click" HorizontalAlignment="Right" Margin="10,15,10,10">Process</Button> <Button Name="btnCancel" Width="100" Click="btnCancel_Click" HorizontalAlignment="Right" Margin="10,0">Cancel</Button> <ProgressBar Name="progressBar" Height="20" Width="200" Margin="10" HorizontalAlignment="Right"/> </StackPanel> </Window>
按如下的代码修改后台代码文件MainWindow.xaml.cs:
using System.Threading; using System.Threading.Tasks; using System.Windows; namespace WpfAwait { public partial class MainMindow : Window { CancellationTokenSource _cancellationTokenSource; CancellationToken _cancellationToken; public MainMindow() { InitializeComponent(); } private async void btnProcess_Click( object sender, RoutedEventArgs e ) { btnProcess.IsEnabled = false; _cancellationTokenSource = new CancellationTokenSource(); _cancellationToken = _cancellationTokenSource.Token; int completedPercent = 0; for ( int i = 0; i < 10; i++) { if ( _cancellationToken.IsCancellationRequested ) break; try { await Task.Delay( 500, _cancellationToken ); completedPercent =( i + 1 ) * 10; } catch ( TaskCanceledException ex ) { completedPercent = i * 10; } progressBar.Value = completedPercent; } string message = _cancellationToken.IsCancellationRequested ? string.Format("Process was cancelled at {0}%.", completedPercent) :"Process completed normally."; MessageBox.Show( message, "Completion Status"); progressBar.Value = 0; btnProcess.IsEnabled = true; btnCancel.IsEnabled = true; } private void btnCancel_Click( object sender, RoutedEventArgs e ) { if ( !btnProcess.IsEnabled ) { btnCancel.IsEnabled = false; _cancellationTokenSource.Cancel(); } } } }
BackgroundWorker类
前面几节介绍了如何使用async/await
特性来异步地处理任务。本节将学习另一种实现异步工作的方式——即后台线程。async/await
特性更适合那些需要在后台完成的不相关的小任务。
但有时候,你可能需要另建一个线程,在后台持续运行以完成某项工作,并不时地与主线程进行通信。BackgroundWorker
类就是为此而生。下图展示了此类的主要成员。
- 图中一开始的两个属性用于设置后台任务是否可以把它的进度汇报给主线程以及是否支持从主线程取消。可以用第三个属性来检査后台任务是否正在运行
- 类有三个事件,用于发送不同的程序事件和状态。你需要为自己的程序写这些事件的事件处理方法来执行适合程序的行为
- 在后台线程开始的时候触发
DoWork
- 在后台任务汇报状态的时候触发
ProgressChanged
事件 - 后台工作线程退出的时候触发
RunWorkerCompleted
事件
- 在后台线程开始的时候触发
- 三个方法用于初始化行为或改变状态
- 调用
RunWorkerAsync
方法获取后台线程并且执行DoWork
事件处理程序 - 调用
CancelAsync
方法把CancellationPending
属性设置为true
。DoWork
事件处理程序需要检查这个属性来决定是否应该停止处理 DoWork
事件处理程序(在后台线程)在希望向主线程汇报进度的时候,调用ReportProgress
方法
- 调用
要使用BackgroundWorker
类对象,需要写如下的事件处理程序。第一个是必需的,因为它包含你希望在后台线程执行的代码,另外两个是可选的,是否使用取决于程序需要。
- 附加到
DoWork
事件的处理程序包含你希望在后台独立线程上执行的代码。- 在下图中,叫做
DoTheWork
的处理程序用渐变的方块表示,表明它在独立的线程中执行 - 主线程调用
RunWorkerAsync
方法的时候触发DoWork
事件
- 在下图中,叫做
- 这个后台线程通过调用
ReportProgress
方法与主线程通信。届时将触发ProgressChanged
事件,主线程可以处理附加到ProgressChanged
事件上的处理程序 - 附加到
RunWorkerCompleted
事件的处理程序应该包含后台线程完成DoWork
亊件处理程序的执行之后需要执行的代码。
下演示了程序的结构,以及附加到BackgroundWorker
对象事件的事件处理程序。
这些事件处理程序的委托如下。每一个任务都有一个object
对象的引用作为第一个参数,以及EventArgs
类的特定子类作为第二个参数。
void DoWorkEventHandler ( object sender, DoWorkEventArgs e ) void ProgressChangedEventHandler ( object sender, ProgressChangedEventArgs e ) void RunWorkerCompletedEventHandler ( object sender, RunWorkerCompletedEventArgs e)
下图演示了这些事件处理程序的EventArg
类的结构。
如果你编写了这些事件处理程序并将其附加到相应的事件,就可以这样使用这些类。
- 从创建
BackgroundWorker
类的对象并且对它进行配置开始- 如果希望工作线程为主线程回报进度,需要把
WorkerReportsProgress
属性设置为true
- 如果希望从主线程取消工作线程,就把
WorkerSupportsCancellation
属性设置为true
- 既然对象已经配置好了,我们就可以通过调用
RunWorkerAsync
方法来启动它。它会开一个后台线程并且发起DoWork
事件并在后台执行事件处理程序
- 如果希望工作线程为主线程回报进度,需要把
现在我们已经运行了主线程以及后台线程。尽管后台线程正在运行,你仍然可以继续主线程的处理。
在主线程中,如果你已经启用了WorkerSupportsCancellation
属性,然后可以调用对象的CancelAsync
方法。和本章开头介绍的CancellationToken
一样,它也不会取消后台线程。而是将对象的CancellationPending
属性设置为true
。运行在后台线程中的DoWork
事件处理程序代码需要定期检査CancellationPending
属性,来判断是否需要退出。
同时在后台线程继续执行其计算任务,并且做以下几件事情。
- 如果
WorkerReportsProgress
属性是true
并且后台线程需要为主线程汇报进度的话,必须调用BackgroundWorker
对象的ReportProgress
方法。这会触发主线程的ProgressChanged
事件,从而运行相应的事件处理程序 - 如果
WorkerSupportsCancellation
属性启用的话,DoWork
事件处理程序代码应该经常检测CancellationPending
属性来确定是否已经取消了。如果是的话,则应该退出 - 如果后台线程没有取消完成了其处理的话,可以通过设置
DoWorkEventArgs
参数的Result
字段来返回结果给主线程,这在上图中已经说过了。
在后台线程退出的时候会触发RunWorkerCompleted
事件,其事件处理程序在主线程上执行。RunWorkerCompletedEventArgs
参数可以包含已完成后台线程的一些信息,比如返回值以及线程是否被取消了。
在WPF程序中使用BackgroundWorker类的示例代码
BackgroundWorker类主要用于GUI程序,下面的程序展示了一个简单的WPF程序。
该程序会生成下图中左图所示的窗体。点击Process按钮将开启后台线程,每半秒向主线程报告一次,并使进度条增长10%。最终,将展示右图所示的对话框。
要创建这个WPF程序,需要在Visual Studio中创建名为SimpleWorker的WPF应用程序。将MainWindow.xaml文件中的代码修改为:
<Window x:Class="SimpleWorker.MainMindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="l50" Width="250"> <StackPanel> <ProgressBar Name="progressBar" Height="20" Width="200" Margin="10"/> <Button Name="btnProcess" Width="l00" Click="btnProcess_Click" Margin="5">Process</Button> <Button Name="btnCancel" Width="l00" Click="btnCancel_Click" Margin="5">Cancel</Button> </StackPanel> </Window>
将MainWindow.xaml.cs文件中的代码修改为:
using System.Windows; using System.ComponentModel; using System.Threading; namespace SimpleWorker { public partial class MainWindow : Window { BackgroundWorker bgWorker = new BackgroundWorker(); public MainWindow() { InitializeComponent(); //设置BackgroundWorker 属性 bgWorker.WorkerReportsProgress = true; bgWorker.WorkerSupportsCancellation = true; //连接BackgroundWorker对象的处理程序 bgWorker.DoWork += DoWork_Handler; bgWorker.ProgressChanged += ProgressChanged_Handler; bgWorker.RunWorkerCompleted += RunWorkerCompleted_Handler; } private void btnProcess_Click( object sender, RoutedEventArgs e) { if ( !bgWorker.IsBusy ) bgWorker.RunWorkerAsync(); } private void ProgressChanged_Handler( object sender,ProgressChangedEventArgs args ) { progressBar.Value = args.ProgressPercentage; } private void DoWork_Handler( object sender, DoWorkEventArgs args ) { BackgroundWorker worker = sender as BackgroundWorker; for ( int i = 1; i <= 10; i++ ) { if ( worker.CancellationPending ) { args.Cancel = true; break; } else { worker.ReportProgress( i * 10 ); Thread.Sleep( 500 ); } } } private void RunWorkerCompleted_Handler( object sender,RunWorkerCompletedEventArgs args ) { progressBar.Value = 0; if ( args.Cancelled ) MessageBox.Show( "Process was cancelled.", "Process Cancelled"); else MessageBox.Show( "Process completed normally.", "Process Completed" ); } private void btnCancel_Click( object sender, RoutedEventArgs e ) { bgWorker.CancelAsync(); } } }
并行循环
本节将简要介绍任务并行库(Task Parellel Library)。它是BCL中的一个类库,极大地简化了并行编程。其细节比本章要介绍的多得多。所以,我在这里只能通过介绍其中的两个简单的结构作为开胃菜了,这样你可以快速并很容易地入门,它们是Parallel.For
循环和Parallel.ForEach
循环。这两个结构位于System.Threading.Tasks
命名空间中。
至此,我相信你应该很熟悉C#的标准for和foreach循环了。这两个结构非常普遍,且极其强大。许多时候我们的循环结构的每一次迭代依赖于之前那一次迭代的计算或行为。但有的时候又不是这样。如果迭代之间彼此独立,并且程序运行在多核处理器的机器上,若能将不同的迭代放在不同的处理器上并行处理的话,将会获益匪浅。Parallel.For
和Parallel.ForEach
结构就是这样做的。
这些构造的形式是包含输入参数的方法。Parallel.For
方法有12个重载,最简单的签名如下。
public static ParallelLoopResult.For( int fromInclusive, int toExclusive, Action body);
fromInclusive
参数是迭代系列的第一个整数toExclusive
参数是比迭代系列最后一个索引号大1的整数。也就是说,和表达式index<ToExclusive
—样body
是接受单个输入参数的委托,body
的代码在每一次迭代中执行一次
如下代码是使用Parallel.For
结构的例子。它从0到14迭代(记住实际的参数15超出了最大迭代索引)并且打印出迭代索引和索引的平方。该应用程序满足各个迭代之间是相互独立的条件。还要注意,必须使用System.Threading.Tasks
命名空间。
using System; using System.Threading.Tasks; // 必须使用这个命名空间 namespace ExampleParallelFor { class Program { static void Main() { Parallel.For(0,15,i=> Console.WriteLine("The square of {0} is {1}",i,i*i)); } } }
在一个四核处理器的PC上运行这段代码产生如下输出。注意,不能确保迭代的执行次序。
The square of 0 is 0 The square of 6 is 36 The square of 7 is 49 The square of 8 is 64 The square of 10 is 100 The square of 11 is 121 The square of 13 is 169 The square of 14 is 196 The square of 4 is 16 The square of 5 is 25 The square of 1 is 1 The square of 2 is 4 The square of 9 is 81 The square of 12 is 144 The square of 3 is 9
另一个示例如下。程序以并行方式填充一个整数数组,把值设置为迭代索引号的平方。
class Program { static void Main() { const int maxValues=50; int[] squares=new int[maxValues]; Parallel.For(0,maxValues,i=>squares[i]=i*i); } }
在本例中,即使迭代在执行时可能为并行并且为任意顺序。但是最后结果始终是一个包含前50个平方数的数组——并且按顺序排列。
另外一个并行循环结构是Parallel.ForEach
方法。该方法有相当多的重载,其中最简单的如下:
TSource
是集合中对象的类型source
是一组TSource
对象的集合body
是要应用到集合中每一个元素的Lambda表达式
static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> source,Action<TSource> body)
使用Paralle.ForEach
方法的例子如下。在这里,TSource
是string
,source
是string[]
。
using System; using System.Threading.Tasks; namespace ParallelForeach1 { class Program { static void Main() { string[] squares=new string[] {"We","hold","these","truths","to","be","self-evident","that","all","men","are","created","equal"}; Parallel.ForEach(squares, i=>Console.WriteLine(string.Format("{0} has {1} letters",i,i.Length))); } } }
在一个四核处理器的PC上运行这段代码产生如下输出,但是每一次运行都可能会有不一样的顺序。
We has 2 letters men has 3 letters truths has 6 letters self-evident has 12 letters equal has 5 letters are has 3 letters created has 7 letters to has 2 letters be has 2 letters hold has 4 letters these has 5 letters that has 4 letters all has 3 letters
其他异步编程模式
如果我们要自己编写异步代码,最可能使用的就是本章前面介绍的async/await
特性和BackgroundWorker
类,或者任务并行库。然而,你仍然有可能需要使用旧的模式来产生异步代码。为了保持完整性,我将从现在开始介绍这些模式,直到本章结束。在学习了这些旧模式后,你将对async/await
特性是多么简单有更加深刻的认识。
第13章介绍了委托的主题,并且了解到当委托对象调用时,它调用了它的调用列表中包含的方法。就像程序调用方法一样,这是同步完成的。
如果委托对象在调用列表中只有一个方法(之后会叫做引用方法),它就可以异步执行这个方法。委托类有两个方法,叫做BeginInvoke
和EndInvoke
,它们就是用来这么做的。这些方法以如下方式使用。
- 当我们调用委托的
BeginInvoke
方法时,它开始在一个独立线程上执行引用方法,并且立即返回到原始线程。原始线程可以继续,而引用方法会在线程池的线程中并行执行 - 当程序希望获取已完成的异步方法的结果时,可以检查
BeginInvoke
返回的IAsyncResult
的IsCompleted
属性,或调用委托的EndInvoke
方法来等待委托完成
下图演示了使用这一过程的三种标准模式。对于这三种模式来说,原始线程都发起了一个异步方法,然后做一些其他处理。然而,这些模式的区別在于,原始线程如何知道发起的线程已经完成。
- 在等待一直到完成(wait-until-done )模式中,在发起了异步方法以及做了一些其他处理之后,原始线程就中断并且等异步方法完成之后再继续
- 在轮询(polling )模式中,原始线程定期检查发起的线程是否完成,如果没有则可以继续做一些其他的事情
- 在回调(callback)模式中,原始线程一直执行,无需等待或检査发起的线程是否完成。在发起的线程中的引用方法完成之后,发起的线程就会调用回调方法,由回调方法在调用
EndInvoke
之前处理异步方法的结果
BeginInvoke 和 EndInvoke
在学习这些异步编程模式的示例之前,让我们先研究一下BeginInvoke
和EndInvoke
方法。一些需要了解的有关BeginInvoke
的重要事项如下。
- 在调用
BeginInvoke
时,参数列表中的实际参数组成如下- 引用方法需要的参数
- 两个额外的参数——
callback
参数和state
参数
BeginInvoke
从线程池中获取一个线程并且让引用方法在新的线程中开始运行BeginInvoke
返回给调用线程一个实现IAsyncResult
接口的对象的引用。这个接口引用包含了在线程池线程中运行的异步方法的当前状态,原始线程然后可以继续执行。
如下的代码给出了一个调用委托的BeginInvoke
方法的示例。第一行声明了MyDel
委托类型。下一行声明了一个和委托匹配的Sum
的方法。
- 之后的行声明了一个叫做
del
的MyDel
委托类型的委托对象,并且使用Sum
方法来初始化它的调用列表 - 最后一行代码调用了委托对象的
BeginInvoke
方法并且提供了两个委托参数3和5,以及两个BeginInvoke
的参数callback
和state
,在本例中都设为null
。执行后,BeginInvoke
方法进行两个操作- 从线程池中获取一个线程并且在新的线程上开始运行
Sum
方法,将3和5作为实参 - 它收集新线程的状态信息并且把
IAsyncResult
接口的引用返回给调用线程来提供这些信息。调用线程把它保存在一个叫做iar的变量中
- 从线程池中获取一个线程并且在新的线程上开始运行
delegate long MyDel(int first,int second);//委托声明 ... static long Sum(int x,int y){...} //方法匹配委托 ... MyDel del=new MyDel(Sum); IAsyncResult iar=del.BeginInvoke(3,5,null,null);
EndInvoke
方法用来获取由异步方法调用返回的值,并且释放线程使用的资源。EndInvoke
有如下的特性。
- 它接受一个由
BeginInvoke
方法返回的IAsyncResult
对象的引用,并找到它关联的线程 - 如果线程池的线程已经退出,
EndInvoke
做如下的事情- 它清理退出线程的状态并且释放其资源
- 它找到引用方法返回的值并且把它作为返回值
- 如果当
EndInvoke
被调用时线程池的线程仍然在运行,调用线程就会停止并等待,直到清理完毕并返回值。因为EndInvoke
是为开启的线程进行清理,所以必须确保对每一个BeginInvoke
都调用EndInvoke
- 如果异步方法触发了异常,在调用
EndInvoke
时会抛出异常
如下的代码行给出了一个调用EndInvoke
并从异步方法获取值的示例。我们必须把IAsyncResult
对象的引用作为参数。
委托对象 ↓ long result=del.EndInvoke(iar); ↑ ↑ 异步方法返回值 IAsyncResult对象
EndInvoke
提供了从异步方法调用的所有输出,包括ref
和out
参数。如果委托的引用方法有ref
或out
参数,它们必须包含在EndInvoke
的参数列表中,并且在IAsyncResult
对象引用之前,如下所示:
long result=del.EndInvoke(out someInt,iar); ↑ ↑ ↑ 异步方法返回值 Out参数 IAsyncResult对象
等待一直到结束模式
既然我们已经理解了BeginInvoke
和EndInvoke
方法,那么就让我们来看看异步编程模式吧。
我们要学习的第一种异步编程模式是等待一直到结束模式。在这种模式里,原始线程发起一个异步方法的调用,做一些其他处理,然后停止并等待,直到开启的线程结束。它总结如下:
IAsyncResult iar = del.BeginInvoke( 3, 5, null, null ); //在发起线程中异步执行方法的同时, //在调用线程中处理一些其他事情 ... long result = del.EndInvoke( iar );
如下代码给出了一个使用这种模式的完整示例。代码使用Thread
类的Sleep
方法将它自己挂起0.1秒。Thread
类在System.Threading
命名空间下。
using System; using System.Threading; // Thread.Sleep() delegate long MyDel( int first, int second ); //声明委托类型 class Program { static long Sum(int x, int y) //声明异步方法 { Console. WriteLine(" Inside Sum"); Thread.Sleep(100); return x + y; } static void Main( ) { MyDel del = new MyDel(Sum); Console.WriteLine( "Before BeginInvoke"); IAsyncResult iar = del.BeginInvoke(3, 5, null, null); //开抬异步调用 Console.WriteLine( "After BeginInvoke"); Console.WriteLine( "Doing stuff" ); long result = del.EndInvoke( iar ); //等待结果并获取结果 Console.WriteLine( "After EndInvoke: {0}", result ); } }
等待一直到结束(wait-until-done)模式的输出:
Before BeginInvoke After BeginInvoke Doing stuff Inside Sum After EndInvoke: 8
AsyncResult类
既然我们已经看到了BeginInvoke
和EndInvoke
的最简单形式,是时候来进一步接触IASyncResult
了。它是使用这些方法的必要部分。
BeginInvoke
返回一个IASyncResult
接口的引用(内部是AsyncResult
类的对象)。AsyncResult
类表现了异步方法的状态。下图演示了该类中的一些重要部分。
有关该类的重要事项如下。
- 当我们调用委托对象的
BeginInvoke
方法时,系统创建了一个AsyncResult
类的对象。然而,它不返回类对象的引用,而是返回对象中包含的IAsyncResult
接口的引用 AsyncResult
对象包含一个叫做AsyncDelegate
的属性,它返回一个指向被调用来开启异步方法的委托的引用。但是,这个属性是类对象的一部分而不是接口的一部分IsCompleted
属性返回一个布尔值,表示异步方法是否完成AsyncState
属性返回一个对象的引用,作为BeginInvoke
方法调用时的state
参数。它返回object
类型的引用,我们会在回调模式一节中解释这部分内容
轮询模式
在轮询模式中,原始线程发起了异步方法的调用,做一些其他处理,然后使用IAsyncResult
对象的IsComplete
属性来定期检査开后的线程是否完成。如果异步方法已经完成,原始线程就调用EndInvoke
并继续。否则,它做一些其他处理,然后过一会儿再检査。在下面的示例中,“处理” 仅仅是由0数到10 000 000。
delegate long MyDel(int first, int second); class Program { static long Sum(int x, int y) { Console.WriteLine(" Inside Sum"); Thread.Sleep(100); return x + y; } static void Main() { MyDel del = new MyDel(Sum);发起异步方法 ↓ IAsyncResult iar = del.BeginInvoke(3, 5, null, null); //开始异步谓用 Console.WriteLine("After BeginInvoke"); 检查异步方法是否完成 ↓ while ( !iar.IsCompleted ) { Console.WriteLine("Not Done"); //继续处理 for (long i = 0; i < 10000000; i++) ; } Console.WriteLine("Done"); 调用EndInvoke来获取接口并进行清理 ↓ long result = del.EndInvoke(iar); Console.WriteLine("Result: {0}", result); } }
轮询(polling)模式的输出:
After BeginInvoke Not Done Inside Sum Not Done Not Done Not Done Not Done Done Result: 8
回调模式
在之前的等待一直到结束(wait-until-done)模式以及轮询(polling)模式中,初始线程继续它自己的控制流程,直到它知道开启的线程已经完成。然后,它获取结果并继续。
回调模式的不同之处在于,一旦初始线程发起了异步方法,它就自己管自己了,不再考虑同步。当异步方法调用结束之后,系统调用一个用户自定义的方法来处理结果,并且调用委托的EndInvoke
方法。这个用户自定义的方法叫做回调方法或回调。
BeginInvoke
的参数列表中最后的两个额外参数由回调方法使用。
- 第一个参数
callback
,是回调方法的名字 - 第二个参数
state
,可以是null
或要传入回调方法的一个对象的引用。我们可以通过使用IAsyncResult
参数的AsyncState
属性来获取这个对象,参数的类型是object
回调方法
回调方法的签名和返回类型必须和AsyncCallback
委托类型所描述的形式一致。它需要方法接受一个IAsyncResult
作为参数并且返回类型是void
,如下所示:
void AsyncCallback( IAsyncResult iar )
我们有多种方式可以为BeginInvoke
方法提供回调方法。由于BeginInvoke
中的callback
参数是AsyncCallback
类型的委托,我们可以以委托形式提供,如下面的第一行代码所示。或者,我们也可以只提供回调方法名称,让编译器为我们创建委托,两种形式是完全等价的。
使用回调方法创建委托 ↓ IAsyncResult iar1 =del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null); 只需要用回调方法的名字 ↓ IAsyncResult iar2 = del.BeginInvoke(3, 5, CallWhenDone, null);
BeginInvoke
的另一个参数是发送给回调方法的对象。它可以是任何类型的对象,但是参数类型是object
,所以在回调方法中,我们必须转换成正确的类型。
在回调方法内调用EndInvoke
在回调方法内,我们的代码应该调用委托的EndInvoke
方法来处理异步方法执行后的输出值。要调用委托的EndInvoke
方法,我们肯定需要委托对象的引用,而它在初始线程中,不在开启的线程中。
如果不使用BeginInvoke
的state
参数作其他用途,可以使用它发送委托的引用给回调方法,如下所示:
结合后面的实例看,不将委托对象作为参数传入也可以在回调函数内部获取
AsyncResult
类对象。
这样看来这个位置更应该传入需要在回调函数中处理或用到的其它对象。
委托对象 把委托对象作为状态参数 ↓ ↓ IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone, del);
然后,我们可以从发送给方法作为参数的IAsyncResult
对象中提取出委托的引用。如下面的代码所示。
- 给回调方法的参数只有一个,就是刚结束的异步方法的
IAsyncResult
接口的引用。请记住,IAsyncResult
接口对象在内部就是AsyncResult
类对象 - 尽管
IAsyncResult
接口没有委托对象的引用,而封装它的AsyncResult
类对象却有委托对象的引用。所以,示例代码方法体的第一行就通过转换接口引用为类类型来获取类对象的引用。变量ar
现在就有类对象的引用 - 有了类对象的引用,我们现在就可以调用类对象的
AsyncDelegate
属性并且把它转化为合适的委托类型。这样就得到了委托引用,我们可以用它来调用EndInvoke
using System.Runtime.Remoting.Messaging; //包含AsyncResult类 void CallWhenDone( IAsyncResult iar ) { AsyncResult ar = (AsyncResult) iar; MyDel del = (MyDel) ar.AsyncDelegate; //获取委托的引用 long Sum = del.EndInvoke( iar ); //调用 EndInvoke ... }
下面把所有知识点放在一起,给出一个使用回调模式的完整示例。
using System; using System.Runtime.Remoting.Messaging;//调用AsyncResult类库 using System.Threading; delegate long MyDel(int first, int second); class Program { static long Sum(int x, int y) { Console.WriteLine(" Inside Sum"); Thread.Sleep(100); return x + y; } static void CallWhenDone(IAsyncResult iar) { Console.WriteLine(" Inside CallWhenDone."); AsyncResult ar = (AsyncResult) iar; MyDel del = (MyDel)ar.AsyncDelegate; long result = del.EndInvoke(iar); Console.WriteLine(" The result is: {0}.",result); } static void Main() { MyDel del = new MyDel(Sum); Console.WriteLine("Before BeginInvoke"); IAsyncResult iar =del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null); Console.WriteLine("Doing more work in Main."); Thread.Sleep(500); Console.WriteLine("Done with Main. Exiting."); Console.ReadKey(); } }
回调(callback)模式的输出:
Before BeginInvoke Doing more work in Main. Inside Sum Inside CallWhenDone. The result is: 8. Done with Main. Exiting.
计时器
计时器提供了另外一种定期地重复运行异步方法的方式。尽管在.NET BCL中有好几个可用的Timer
类,但在这里我们只会介绍System.Threading
命名空间中的那个。
有关计时器类需要了解的重要事项如下。
- 计时器在每次时间到期之后调用回调方法。回调方法必须是
TimerCallback
委托形式的,结构如下所示。它接受一个object
类型作为参数,并且返回类型是void
void TimerCallback( object state )
- 当计时器到期之后,系统会从线程池中的线程上开启一个回调方法,提供
state
对象作为其参数,并且开始运行 - 我们可以设置的计时器的一些特性如下
dueTime
是回调方法首次被调用之前的时间。如果dueTime
被设为特殊的值Timeout.Infinite
,则计时器不会开始。如果被设置为0
,回调函数会被立即调用period
是两次成功调用回调函数之间的时间间隔。如果它的值设置为Timeout.Infinite
,回调在首次被调用之后不会再被调用state
可以是null
或在每次回调方法执行时要传入的对象的引用
Timer
类的构造函数接受回调方法名称、dueTime
、period
以及state
作为参数。Timer
有很多构造函数,最为常用的形式如下:
Timer(TimerCallback callback,object state,uint dueTime,uint period)
例:创建Timer
对象的示例:
回调的 在2000毫秒后 名字 第一次调用 ↓ ↓ Timer myTimer = new Timer ( MyCallback, someObject, 2000, 1000 ); ↑ ↑ 传给回调的 每1000毫秒 对象 调用一次
一旦Timer
对象被创建,我们可以使用Change
方法来改变它的dueTime
或period
方法。
如下代码给出了一个使用计时器的示例。Main
方法创建一个计时器,2秒钟之后它会首次调用回调,然后每隔1秒调用1次。回调方法只是输出了包含它被调用的次数的消息。
using System; using System.Threading; namespace Timers { class Program { int TimesCalled = 0; void Display(object state) { Console.WriteLine("{0} {1}", (string)state, ++TimesCalled); } static void Main() { Program p = new Program(); Timer myTimer = new Timer //2s后第一次调用,每1s重复依次 (p.Display, "Processing timer event", 2000, 1000); Console.WriteLine("Timer started."); Console.ReadLine(); } } }
输出:
Timer started. Processing timer event 1 Processing timer event 2 Processing timer event 3 Processing timer event 4 Processing timer event 5
.NET BCL还提供了几个其他计时器类,每一个都有其用途。其他计时器类如下所示。
System.Windows.Forms.Timer
这个类在Windows应用程序中使用,用来定期把WM_TIMER
消息放到程序的消息队列中。当程序从队列获取消息后,它会在主用户接口线程中同步处理,这对Windows应用程序来说非常重要System.Timers.Timer
这个类更复杂,它包含了很多成员,使我们可以通过属性和方法来操作计时器。它还有一个叫做Elapsed
的成员事件,每次时间到期就会发起这个事件。这个计时器可以运行在用户接口线程或工作者线程上
作者:Moonache
出处:http://www.cnblogs.com/moonache/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。