C# 5.0 搭载于.NET 4.5和VS2012之上。html
同步操做既简单又方便,咱们平时都用它。可是对于某些状况,使用同步代码会严重影响程序的可响应性,一般来讲就是影响程序性能。这些状况下,咱们一般是采用异步编程来完成功能,这在前面也屡次说起了。异步编程的核心原理也就是使用多线程/线程池和委托来完成任务的异步执行和返回,只不过在每一个新的C#版本中,微软都替咱们完成了更多的事,使得程序模板愈来愈傻瓜化了。程序员
.NET Framework 提供如下两种执行 I/O 绑定和计算绑定异步操做的标准模式:
1. 异步编程模型 (APM,Asynchronous Programming Model)编程
在该模型中异步操做由一对 Begin/End 方法(如 FileStream.BeginRead 和 Stream.EndRead)表示。
异步编程模型是一种模式,该模式使用更少的线程去作更多的事。.NET Framework不少类实现了该模式,这些类都定义了BeginXXX和EndXXX相似的方法,好比FileStream类的BeginRead和EndRead方法。同时咱们也能够自定义类来实现该模式(也就是在自定义的类中实现返回类型为IAsyncResult接口的BeginXXX方法和EndXXX方法);另外委托类型也定义了BeginInvoke和EndInvoke方法,使得委托能够异步执行。这些异步操做的背后都是线程池在支撑着,这是微软异步编程的基础架构,也是比较老的模式,不过从中咱们能够清楚的了解异步操做的原理。多线程
全部BeginXXX方法返回的都是实现了IAsyncResult接口的一个对象,并非对应的同步方法所要获得的结果的。此时咱们须要调用对应的EndXXX方法来结束异步操做,并向该方法传递IAsyncResult对象,EndXxx方法的返回类型就是和同步方法同样的。例如,FileStream的EndRead方法返回一个Int32来表明从文件流中实际读取的字节数。架构
对于访问异步操做的结果,APM提供了四种方式供开发人员选择:异步
- 在调用BeginXxx方法的线程上调用EndXXX方法来获得异步操做的结果,可是这种方式会阻塞调用线程,直到操做完成以后调用线程才继续运行
- 查询IAsyncResult的AsyncWaitHandle属性,从而获得WaitHandle,而后再调用它的WaitOne方法来使一个线程阻塞并等待操做完成再调用EndXxx方法来得到操做的结果。
- 循环查询IAsyncResult的IsComplete属性,操做完成后再调用EndXxx方法来得到操做返回的结果。
- 使用 AsyncCallback委托来指定操做完成时要调用的方法,在操做完成后调用的方法中调用EndXxx操做来得到异步操做的结果。
在上面的4种方式中,第4种方式是APM的首选方式,由于此时不会阻塞执行BeginXxx方法的线程,然而其余三种都会阻塞调用线程,至关于效果和使用同步方法是同样,在实际异步编程中都是使用委托的方式。async
看一个简答的例子:异步编程
using System; using System.Net; using System.Threading; class Program { static DateTime start; static void Main(string[] args) { // 用百度分别检索0,1,2,3,4,共检索5次 start = DateTime.Now; string strReq = "http://www.baidu.com/s?wd={0}"; for (int i = 0; i < 5; i++) { var req = WebRequest.Create(string.Format(strReq, i)); // 注意这里的BeginGetResponse就是异步方法 var res = req.BeginGetResponse(ProcessWebResponse, req); } Thread.Sleep(1000000); } private static void ProcessWebResponse(IAsyncResult result) { var req = (WebRequest)result.AsyncState; string strReq = req.RequestUri.AbsoluteUri; using (var res = req.EndGetResponse(result)) { Console.Write("检索 {0} 的结果已经返回!\t", strReq.Substring(strReq.Length - 1)); Console.WriteLine("耗用时间:{0}毫秒", TimeSpan.FromTicks(DateTime.Now.Ticks - start.Ticks).TotalMilliseconds); } } }
结构至关简单,使用了回调函数获取结果,就很少说了。函数
2. 基于事件的异步模式 (EAP,Event based Asynchronous programming Model)性能
在该模式中异步操做由名为“XXXAsync”和“XXXCompleted”的方法/事件表示,例如WebClient.DownloadStringAsync 和 WebClient.DownloadStringCompleted,还有像经常使用的BackgroundWorker.RunWorkerAsync和BackgroundWorker.RunWorkerCompleted方法。
EAP 是在 .NET Framework 2.0 版中引入的。使用陈旧的BeginXXX和EndXXX方法无疑是不够优雅的,而且程序员须要写更多的代码,特别是在UI程序中使用不太方便。UI的各类操做基本都是基于事件的,并且一般来讲UI线程和子线程之间还须要互相交流,好比说显示进度,警告,相关的消息等等,直接在子线程中访问UI线程上的空间是须要写一些同步代码的。这些操做使用APM处理起来都比较麻烦,而EAP则很好的解决了这些问题,EAP里面最出色的表明就应该是BackgroundWorker类了。
看一个网上一位仁兄写的下载的小例子:
private void btnDownload_Click(object sender, EventArgs e) { if (bgWorkerFileDownload.IsBusy != true) { // 开始异步执行DoWork中指定的任务 bgWorkerFileDownload.RunWorkerAsync(); // 建立RequestState对象 requestState = new RequestState(downloadPath); requestState.filestream.Seek(DownloadSize, SeekOrigin.Begin); this.btnDownload.Enabled = false; this.btnPause.Enabled = true; } else { MessageBox.Show("正在执行操做,请稍后"); } } private void btnPause_Click(object sender, EventArgs e) { // 暂停的标准处理方式:先判断标识,而后异步申请暂停 if (bgWorkerFileDownload.IsBusy && bgWorkerFileDownload.WorkerSupportsCancellation == true) { bgWorkerFileDownload.CancelAsync(); } } // 指定Worker的工做任务,当RunWorkerAsync方法被调用时开始工做 // 这是在子线程中执行的,不容许访问UI上的元素 private void bgWorkerFileDownload_DoWork(object sender, DoWorkEventArgs e) { // 获取事件源 BackgroundWorker bgworker = sender as BackgroundWorker; // 开始下载 HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim()); // 断点续传的功能 if (DownloadSize != 0) { myHttpWebRequest.AddRange(DownloadSize); } requestState.request = myHttpWebRequest; requestState.response = (HttpWebResponse)myHttpWebRequest.GetResponse(); requestState.streamResponse = requestState.response.GetResponseStream(); int readSize = 0; // 前面讲过的异步取消中子线程的工做:循环并判断标识 while (true) { if (bgworker.CancellationPending == true) { e.Cancel = true; break; } readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length); if (readSize > 0) { DownloadSize += readSize; int percentComplete = (int)((float)DownloadSize / (float)totalSize * 100); requestState.filestream.Write(requestState.BufferRead, 0, readSize); // 报告进度,引起ProgressChanged事件的发生 bgworker.ReportProgress(percentComplete); } else { break; } } } // 当Worker执行ReportProgress时回调此函数。此函数在UI线程中执行更新操做进度的任务 // 由于是在在主线程中工做的,能够与UI上的元素交互 private void bgWorkerFileDownload_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.progressBar1.Value = e.ProgressPercentage; } // 当Worker结束时触发的回调函数:也许是成功完成的,或是取消了,或者是抛异常了。 // 这个方法是在UI线程中执行,因此能够与UI上的元素交互 private void bgWorkerFileDownload_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { MessageBox.Show(e.Error.Message); requestState.response.Close(); } else if (e.Cancelled) { MessageBox.Show(String.Format("下载暂停,下载的文件地址为:{0}\n 已经下载的字节数为: {1}字节", downloadPath, DownloadSize)); requestState.response.Close(); requestState.filestream.Close(); this.btnDownload.Enabled = true; this.btnPause.Enabled = false; } else { MessageBox.Show(String.Format("下载已完成,下载的文件地址为:{0},文件的总字节数为: {1}字节", downloadPath, totalSize)); this.btnDownload.Enabled = false; this.btnPause.Enabled = false; requestState.response.Close(); requestState.filestream.Close(); } } private void GetTotalSize() { HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(txbUrl.Text.Trim()); HttpWebResponse response = (HttpWebResponse)myHttpWebRequest.GetResponse(); totalSize = response.ContentLength; response.Close(); } // 存储申请的状态 public class RequestState { public int BufferSize = 2048; public byte[] BufferRead; public HttpWebRequest request; public HttpWebResponse response; public Stream streamResponse; public FileStream filestream; public RequestState(string downloadPath) { BufferRead = new byte[BufferSize]; request = null; streamResponse = null; filestream = new FileStream(downloadPath, FileMode.OpenOrCreate); } }
上面的例子就是实现了一个能够取消的带断点续传功能的下载器,这是个Winform程序,控件也很简单:一个Label,一个Textbox,两个Button,一个ProgressBar;把这些控件和上面的事件对应绑定便可。
在.NET 4.0 (C# 4.0)中,并行库(TPL)的加入使得异步编程更加方便快捷,在.NET 4.5 (C# 5.0)中,异步编程将更加方便。
这里咱们先回顾一下C# 4.0中的TPL的用法,看一个简单的小例子:这个例子中只有一个Button和一个Label,点击Button会调用一个函数计算一个结果,这个结果最后会显示到Label上,很简单,咱们只看核心的代码:
private void button1_Click(object sender, EventArgs e) { this.button1.Enabled = false; var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); //get UI thread context var someTask = Task<int>.Factory.StartNew(() => slowFunc(1, 2)); //create and start the Task someTask.ContinueWith(x => { this.label1.Text = "Result: " + someTask.Result.ToString(); this.button1.Enabled = true; }, uiScheduler ); } private int slowFunc(int a, int b) { System.Threading.Thread.Sleep(3000); return a + b; }
上面的slowFunc就是模拟了一个须要大量时间去运行的任务,为了避免阻塞UI线程,只能使用Task去异步运行,为了在把结果显示到Label上,代码中咱们使用了TaskScheduler.FromCurrentSynchronizationContext()方法同步线程上下文,使得在ContinueWith方法中可使用UI线程上的控件,这是TPL编程中的一个经常使用技巧。
说不上太麻烦,可是感受上总之不舒服,彻底没有同步代码写起来那么天然,简单。从我我的的理解来讲,C# 5.0中的async和await正是提升了这方面的用户体验。
C# 5.0中的async和await特性并无在IL层面增长了新的成员,因此也能够说是一种语法糖。下面先看看再C# 5.0中如何解决这个问题:
private async void button1_Click(object sender, EventArgs e) { this.button1.Enabled = false; var someTask = Task<int>.Factory.StartNew(() => slowFunc(1, 2)); await someTask; this.label1.Text = "Result: " + someTask.Result.ToString(); this.button1.Enabled = true; }
注意这段代码中的async和await的用法。除了这个事件处理函数,其余的都没有变化。是否是很神奇,彻底和同步代码没什么太大的区别,非常简单优雅,彻底是同步方式的异步编程。
下面咱们就详细的讨论一下async和await这两个关键字。
async和await
经过使用async修饰符,可将方法、lambda表达式或匿名方法指定为异步。 使用了这个修饰符的方法或表达式,则其称为异步方法,如上面的button1_Click方法就是一个异步方法。
异步方法提供了一种简便方式来完成可能须要长时间运行的工做,而没必要阻塞调用方的线程。 异步方法的调用方(这里就是button1_Click的调用者)能够继续工做,而没必要等待异步方法button1_Click完成。 完成这个特性须要使用 await 关键字,以便当即返回,从而容许button1_Click的调用方继续工做或返回到线程的同步上下文(或消息泵)。
从上面的描述中获得,异步方法更准确的定义应该是:使用async修饰符定义的,且一般包含一个或多个await表达式的方法称为异步方法。
若是async关键字修饰的方法不包含await表达式或语句,则该方法仍将同步执行。 对于这种状况,编译器将会给出警告,由于该状况一般表示程序可能存在错误。 也就是说,单单使用async修饰符的方法仍是在同步执行的,只有配合await关键字后方法的部分才开始异步执行。
await表达式不阻塞主线程。 相反,它告诉编译器去重写异步方法来完成下面几件事:
1. 启动子线程(一般是线程池中的线程)完成await表达式中指定的任务,这是异步执行的真正含义。
2. 将await表达式后面未执行的语句注册为await表达式中执行的任务的后续任务,而后挂起这个异步方法,直接返回到异步方法的调用方。
3. 当await表达式中执行的任务完成后,子线程结束。
4. 任务寻找到注册的后续任务,恢复异步方法的执行环境,继续执行后续任务,由于已经恢复到异步方法的执行上下文中,因此不存在跨线程的问题。
看了这个过程,其实与咱们使用ContinueWith的那种方式没什么太大的不一样。回到上面的button1_Click方法,这下就好理解了,该方法从开始时同步运行,直至到达其第一个await表达式,此时异步的执行Task中指定的方法,而后将button1_Click方法挂起,回到button1_Click的调用者执行其余的代码;直到等待的任务完成后,回到button1_Click中继续执行后续的代码,也就是更新Label的内容。
这里须要注意几点:
1. async和await只是上下文关键字。 当它们不修饰方法、lambda 表达式或匿名方法时,就不是关键字了,只做为普通的标识符。
2. 使用async修饰的异步方法的返回类型能够为 Task、Task<TResult> 或 void。 方法不能声明任何 ref 或 out 参数,可是能够调用具备这类参数的方法。
若是异步方法须要一个 TResult 类型的返回值,则须要应指定 Task<TResult> 做为方法的返回类型。
若是当方法完成时未返回有意义的值,则应使用 Task。 对于返回Task的异步方法,当 Task 完成时,任何等待 Task 的全部 await 表达式的计算结果都为 void。
而使用void做为返回类型的方式主要是来定义事件处理程序,这些处理程序须要此返回类型。 使用void 做为异步方法的返回值时,该异步方法的调用方不能等待,而且没法捕获该方法引起的异常。
3. await表达式的返回值
若是 await 应用于返回Task<TResult>的方法调用的结果,那么 await 表达式的类型是 TResult。 若是将 await 应用于返回Task的方法调用结果,则 await 表达式的类型无效。看下面的例子中的使用方式:
// 返回Task<TResult>的方法. TResult result = await AsyncMethodThatReturnsTaskTResult(); // 返回一个Task的方法. await AsyncMethodThatReturnsTask();
4.异常问题
大多数异步方法返回 Task 或 Task<TResult>。 返回任务的属性承载有关其状态和历史记录的信息,例如任务是否已完成,异步方法是否引起异常或已取消,以及最终结果如何。 await 运算符会访问那些属性。
若是任务返回异常,await 运算符会再次引起异常。
若是任务被取消后返回,await 运算符也会再次引起 OperationCanceledException。
总之,在await外围使用try/catch能够捕获任务中的异常。看一个例子:
public class AsyncTest { static void Main(string[] args) { AsyncTest c = new AsyncTest(); c.RunAsync(); // 模拟其余的工做 Thread.Sleep(1000000); } public void RunAsync() { DisplayValue(); //这里不会阻塞 Console.WriteLine("RunAsync() End."); } public Task<double> GetValueAsync(double num1, double num2) { return Task.Run(() => { for (int i = 0; i < 1000000; i++) { num1 = num1 / num2; if (i == 999999) { throw new Exception("Crash"); } } return num1; }); } public async void DisplayValue() { double result = 0; //此处会开新线程处理GetValueAsync任务,而后方法立刻返回 try { result = await GetValueAsync(1234.5, 1.0); } catch (Exception) { //throw; } //这以后的全部代码都会被封装成委托,在GetValueAsync任务完成时调用 Console.WriteLine("Value is : " + result); } }
可是须要注意一点,若是任务抛出了多个异常(例如,该任务多是启动了更多的子线程)时,await运算符只能抛出异常中的一个,并且不能肯定是哪个。这时就须要把这些子线程包装到一个Task中,这样这些异常就都会被包装到AggregateException中,看下面例子的作法:
public class AsyncTest { static void Main(string[] args) { AsyncTest c = new AsyncTest(); c.RunAsync(); // 模拟其余的工做 Thread.Sleep(1000000); } public void RunAsync() { DisplayValue(); //这里不会阻塞 Console.WriteLine("RunAsync() End."); } public async void DisplayValue() { Task all = null; try { await (all = Task.WhenAll( Task.Run(() => { throw new Exception("Ex1"); }), Task.Run(() => { throw new Exception("Ex2"); })) ); } catch { foreach (var ex in all.Exception.InnerExceptions) { Console.WriteLine(ex.Message); } } } }
固然了,你们也别忘了最后一招杀手锏:TaskScheduler.UnobservedTaskException,使用这个去捕获一些没有处理的异常。
到此,异步方法就介绍到这里了。最后附上一位网上兄弟写的异步执行一些耗时操做的辅助类:
public static class TaskAsyncHelper { /// <summary> /// 将一个方法function异步运行,在执行完毕时执行回调callback /// </summary> /// <param name="function">异步方法,该方法没有参数,返回类型必须是void</param> /// <param name="callback">异步方法执行完毕时执行的回调方法,该方法没有参数,返回类型必须是void</param> public static async void RunAsync(Action function, Action callback) { Func<System.Threading.Tasks.Task> taskFunc = () => { return System.Threading.Tasks.Task.Run(() => { function(); }); }; await taskFunc(); if (callback != null) callback(); } /// <summary> /// 将一个方法function异步运行,在执行完毕时执行回调callback /// </summary> /// <typeparam name="TResult">异步方法的返回类型</typeparam> /// <param name="function">异步方法,该方法没有参数,返回类型必须是TResult</param> /// <param name="callback">异步方法执行完毕时执行的回调方法,该方法参数为TResult,返回类型必须是void</param> public static async void RunAsync<TResult>(Func<TResult> function, Action<TResult> callback) { Func<System.Threading.Tasks.Task<TResult>> taskFunc = () => { return System.Threading.Tasks.Task.Run(() => { return function(); }); }; TResult rlt = await taskFunc(); if (callback != null) callback(rlt); } }
简单实用!
推荐连接:你必须知道的异步编程:http://www.cnblogs.com/zhili/category/475336.html传统异步编程指导:http://msdn.microsoft.com/zh-cn/library/vstudio/dd997423.aspx使用async异步编程指导:http://msdn.microsoft.com/zh-cn/library/vstudio/hh191443.aspx