(一)異步編程的重要性
使用異步編程,方法調用是在後台運行(通常在線程或任務的幫助下),並不會阻塞調用線程。有3中不同的異步編程模式:異步模式、基於事件的異步模式和新增加的基於任務的異步模式(TAP,可利用async和await關鍵字來實現)。
(二)異步模式
1、C#1的APM 異步編程模型(Asynchronous Programming Model)。
2、C#2的EAP 基於事件的異步模式(Event-based Asynchronous Pattern)。
3、TAP 基於任務的異步模式(Task-based Asynchronous Pattern)。
參考:http://www.cnblogs.com/zhaopei/p/async_one.html
(三)異步編程基礎
async和await關鍵字只是編譯器功能。編譯器會用Task類創建代碼。
1、創建任務
1 /// <summary> 2 /// 同步方法 3 /// </summary> 4 /// <returns></returns> 5 static string SayHi(string name) 6 { 7 Thread.Sleep(3000); 8 return "你好!"+ name; 9 } 10 11 /// <summary> 12 /// 基於任務的異步模式 13 /// </summary> 14 /// <returns></returns> 15 static Task<string> SayHiAsync(string name) 16 { 17 return Task.Run<string>(()=> { 18 return SayHi(name); 19 }); 20 }
泛型版本的Task.Run<string>創建一個返回字符串的任務。
2、調用異步方法
使用await關鍵字需要有用async修飾符聲明的方法。在await的方法沒有完成前,該方法內的其他代碼不會繼續執行,但是調用await所在方法的線程不會被阻塞。
private async static void CallerWithAsync() { string result = await SayHiAsync("張三"); Console.WriteLine(result); }
async修飾符只能用於返回Task和void方法。
3、延續任務
Task類的ContinueWith方法定義了任務完成後就調用的代碼。指派給ContinueWith方法的委托接受將已完成的任務作為參數傳入,使用Result屬性可以訪問任務返回的結果。
private static void CallerWithContinuationTask() { Task<string> t = SayHiAsync("李四"); t.ContinueWith(_t => Console.WriteLine(_t.Result)); }
4、同步上下文
WPF應用程序設置了DispatcherSynchronizationContext屬性,WindowsForm應用程序設置了WindowsFormsSynchronizationContext屬性。如果調用異步方法的線程分配給了同步上下文,await完成之後將繼續執行。如果不使用相同的上下文,必須調用Task類的ConfigureAwait(ContinueOnCapturedContext:false)。
5、使用多個異步方法
(1)按順序調用異步方法
private async static void MultipleCallerWithAsync() { string result1 = await SayHiAsync("張三"); string result2 = await SayHiAsync("李四"); Console.WriteLine("完成了兩次打招呼!{0} 和 {1}", result1, result2); }
(2)使用組合器
一個組合器可以接受多個同一類型的參數,並返回同一類型的值。Task組合器接受多個Task對象作為參數,並返回一個Task。
private async static void MultipleCallerWithAsyncWithCombinators1() { Task<string> task1 = SayHiAsync("張三"); Task<string> task2 = SayHiAsync("李四"); await Task.WhenAll(task1,task2); Console.WriteLine("完成了兩次打招呼!{0} 和 {1}", result1, result2); }
Task類定義了WhenAll(全部任務完成才返回)和WhenAny(任意任務完成即返回)兩個組合器。
當任務返回類型相同時,可以用數組接受返回結果。
private async static void MultipleCallerWithAsyncWithCombinators2() { Task<string> task1 = SayHiAsync("張三"); Task<string> task2 = SayHiAsync("李四"); string [] results=await Task.WhenAll(task1,task2); Console.WriteLine("完成了兩次打招呼!{0} 和 {1}", results[0], results[1]); }
6、轉換異步模式
當某些類沒有提供基於任務的異步模式時(僅有BeginXX,EndXX),可以使用TaskFactory類定義的FromAsync方法轉換為基於任務的異步模式的方法。
private static async void ConvertingAsyncPattern() { Func<string, string> method = SayHi; string result = await Task<string>.Factory.FromAsync<string>((name, callback,state) => { return method.BeginInvoke(name, callback, state); },ar=> { return method.EndInvoke(ar); },"王麻子",null); Console.WriteLine(result); }
(四)錯誤處理
1、異步方法的異常處理
異步方法異常的一個較好的處理方式,就是使用await關鍵字,將其放在try/catch語句中。
1 static async Task ThrowAfter(int ms, string message) 2 { 3 await Task.Delay(ms); 4 throw new Exception(message); 5 } 6 7 private static async void HandleOneError() 8 { 9 try 10 { 11 await ThrowAfter(2000, "first"); 12 } 13 catch (Exception ex) 14 { 15 Console.WriteLine("handled {0}", ex.Message); 16 } 17 }
2、多個異步方法的異常處理
在順序調用兩個及以上會拋出異常的方法時,不可再使用以上方法,因為當第一個異步方法拋出異常時try塊裡的余下方法不會再被調用。
如果需要將剩余的方法繼續執行完,再對異常進行處理,可以使用Task.WhenAll方法,這樣不管任務是否拋出異常,都會等到所有任務執行完。但是,也只能看見傳遞給WhenAll方法的第一個異常。
private static async void HandleOneError() { try { Task task1 = ThrowAfter(1000, "first"); Task task2 = ThrowAfter(2000, "second"); await Task.WhenAll(task1, task2); } catch (Exception ex) { Console.WriteLine("handled {0}", ex.Message); } }
如果需要將剩余的方法執行完,且獲取所有拋出異常,可以在try塊外聲明任務變量,使其可以在catch塊內被訪問。這樣可以使用IsFaulted屬性檢查任務的狀態,當為true時,可以使用Task類的Exception.InnerException訪問異常信息。
private static async void HandleOneError() { Task task1 = null; Task task2 = null; try { task1 = ThrowAfter(1000, "first"); task2 = ThrowAfter(2000, "second"); await Task.WhenAll(task1, task2); } catch (Exception ex) { if (task1 != null && task1.IsFaulted) { Console.WriteLine(task1.Exception.InnerException); } if (task2 != null && task2.IsFaulted) { Console.WriteLine(task2.Exception.InnerException); } } }
3、使用AggregateException信息
為了得到所有的異常信息,還可以將Task.WhenAll返回的結果寫入一個Task變量中,然後訪問Task類的Exception屬性(AggregateException類型)。AggregateException定義了InnerExceptions屬性,它包含了所有的異常信息。
private static async void HandleOneError() { Task task = null; try { Task task1 = ThrowAfter(1000, "first"); Task task2 = ThrowAfter(2000, "second"); await (task = Task.WhenAll(task1, task2)); } catch (Exception) { foreach (var exception in task.Exception.InnerExceptions) { Console.WriteLine(exception); } } }
(五)取消
1、取消任務
private CancellationTokenSource cts; private void OnCancel() { if (cts != null) { cts.Cancel(); //cts.CancelAfter(1000);//等待1000ms後取消 } }
2、使用框架特性取消任務
private async void OnTaskBasedAsyncPattern() { List<string> urlList = new List<string>(); urlList.Add("http://www.baidu.com"); cts = new CancellationTokenSource(); try { foreach (var url in urlList) { Random rd = new Random(); int i = rd.Next(1, 100); //1到100之間的數, if (i%2==0) { OnCancel();//當隨機數為偶數時取消任務 } var client = new HttpClient(); var response = await client.GetAsync(url, cts.Token);//GetAsync方法會檢查是否應該取消操作 var result =await response.Content.ReadAsStringAsync(); Console.WriteLine(result); } } catch (OperationCanceledException ex)//當任務取消時會拋出該異常 { Console.WriteLine(ex.Message); } }
3、取消自定義任務
Task類的Run方法提供了傳遞CancellationToken參數的重載版本。使用IsCancellationRequest屬性檢查令牌,用ThrowIfCancellationRequested方法觸發異常。
public async void CustomerTask() { cts = new CancellationTokenSource(); var list = new List<string>(); list.Add("1"); list.Add("2"); list.Add("3"); var deal_list = new List<int>(); try { await Task.Run(() => { foreach (var item in list) { Random rd = new Random(); int i = rd.Next(1, 100); //1到100之間的數, if (i % 2 == 0) { OnCancel();//當隨機數為偶數時取消任務 } if (cts.Token.IsCancellationRequested) { Console.WriteLine("處理任務異常,回滾"); deal_list.Clear(); cts.Token.ThrowIfCancellationRequested(); } deal_list.Add(Convert.ToInt32(item)); Console.WriteLine(item); } }, cts.Token); } catch (OperationCanceledException ex) { Console.WriteLine(ex.Message); } }