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