启动一个程序,系统在内存中建立一个新进程,进程内部是系统建立的线程,线程能够派生其余线程,这就有了多线程。
进程内的多个线程共享进程的资源,系统为处理器规划的单元是线程。程序员
异步编程能够实如今新线程里面运行一部分代码,或改变代码的执行顺序。编程
本章介绍了如下几种异步编程方式,它们居可能是并发的而非并行。数组
async/await
BackgroundWorker
Task Parellel
System.Threading.Tasks
中的Parallel.For
和Parallel.ForEach
BeginInvoke/EndInvoke
System.Threading.Timer
启动程序时,系统会在内存中建立一个新的进程。进程是构成运行程序的资源集合。包括虚地址空间、文件句柄和许多其余程序运行所需的东西。服务器
在进程内部,系统建立了一个称为线程的内核(kernel)对象,它表明了真正执行的程序。(线程是“执行线程”的简称)一旦进程创建,系统会在Main方法的第一行语句处就开始线程的执行。网络
关于线程,须要了解如下知识点多线程
本书目前为止所展现的全部示例程序都只使用了一个线程,而且从程序的第一条语句按顺序执行到最后一条。然而在不少状况下,这种简单的模型都会在性能或用户体验上致使难以接受的行为。架构
例如,一个服务器程序可能会持续不断地发起到其余服务器的链接,并向它们请求数据,同时处理来自多个客户端程序的请求。这种通讯任务每每耗费大量时间,在此期间程序只能等待网络或互联网上其余计算机的响应。这严重削弱了性能。程序不该该浪费等待响应的时间,而应该更加高效,在等待的同时执行其余任务,回复到达后再继续执行第一个任务。并发
本章咱们将学习异步编程。在异步程序中,程序代码不须要按照编写的顺序严格执行。有时须要在一个新的线程中运行一部分代码,有时无需建立新的线程,但为了更好地利用单个线程的能力,须要改变代码的执行顺序。框架
咱们先来看看C#5.0引入的一个用来构建异步方法的新特性——async/await
。接下来学习一些可实现其余形式的异步编程的特性,这些特性是.NET框架的一部分,但没有嵌入C#语言。相关主题包括BackgroundWorker
类和.NET任务并行库。二者均经过新建线程来实现异步。本章最后咱们会看看编写异步程序的其余方式。异步
为了演示和比较,咱们先来看一个不使用异步的示例。而后再看一个实现相似功能的异步程序。
在下面的代码示例中,MyDownloadString类的方法DoRun执行如下任务。
System.Diagnostics
命名空间)的一个实例并启动。该Stopwatch计时器用来测量代码中不一样任务的执行时间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
特性就容许咱们这么作。能够重写代码以运用该特性,以下所示。稍后我会深刻剖析这个特性,如今先来看看本示例须要注意的几个方面。
Task<int>
类型的占位符对象,表示它计划进行的工做。这个占位符最终将“返回”一个intTask<int>
对象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 方法调用等待网站响应的时候进行的。全部这些工做都是在主线程中完成的,咱们没有建立任何额外的线程!
咱们已经看到了一个异步方法的示例,如今来讨论其定义和细节。
若是一个程序调用某个方法,等待其执行全部处理后才继续执行,咱们就称这样的方法是同步的。这是默认形式,在本章以前你所看到的都是这种形式。
相反,异步的方法在处理完成以前就返回到调用方法。C#的async/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>
关键字 返回类型 ↓ ↓ 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表达式时,异步方法将控制返回到调用方法。若是方法的返回类型为Task
或Task<T>
类型,将建立一个Task
对象,表示需异步完成的任务和后续,而后将该Task
返回到调用方法。
目前有两个控制流:异步方法内的和调用方法内的。异步方法内的代码完成如下工做。
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关键字和一个空闲对象 (称为任务)组成。这个任务多是一个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。
Func<int>
委托。而后在下一行将该委托用于Task.Run方法Func<int>
委托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个不一样的位置。
假设咱们的某个方法不符合这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表达式放在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对象,该对象将暂停其在线程中的处理,并在必定时间以后完成。然而与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线程中完成,如点击按钮、展现标签、移动窗体等。Windows程序是经过消息来实现这一点的,消息被放入由消息泵管理的消息队列中。
消息泵从队列中取出一条消息,并调用它的处理程序(handler)代码。当处理程序代码完成时,消息泵获取下一条消息并循环这个过程。
因为这种架构,处理程序代码就必须快捷,这样才不至于挂起并阻碍其余GUI行为的处理。若是某个消息的处理程序代码耗时过长,消息队列中的消息会产生积压。程序将失去响应,由于在那个长时间运行的处理程序完成以前,没法处理任何消息。
下图展现了一个WPF程序中两个版本的窗体。窗体由状态标签及其下方的按钮组成。开发者的目的是,程序用户点击按钮,按钮的处理程序代码执行如下操做:
右图的截屏展现了开发者但愿在按钮按下的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
方法建立一个当即返回的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表达式注册为一个按钮点击事件的事件处理程序。
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>
咱们按部就班地介绍了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(); } } } }
前面几节介绍了如何使用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
参数能够包含已完成后台线程的一些信息,好比返回值以及线程是否被取消了。
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
方法来等待委托完成下图演示了使用这一过程的三种标准模式。对于这三种模式来讲,原始线程都发起了一个异步方法,而后作一些其余处理。然而,这些模式的区別在于,原始线程如何知道发起的线程已经完成。
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
既然咱们已经看到了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
方法,咱们确定须要委托对象的引用,而它在初始线程中,不在开启的线程中。
若是不使用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
的成员事件,每次时间到期就会发起这个事件。这个计时器能够运行在用户接口线程或工做者线程上