前言 本節主要介紹非同步編程中Task、Async和Await的基礎知識。 什麼是非同步? 非同步處理不用阻塞當前線程來等待處理完成,而是允許後續操作,直至其它線程將處理完成,並回調通知此線程。 非同步和多線程 相同點:避免調用線程阻塞,從而提高軟體的可響應性。 不同點: 非同步操作無須額外的線程負擔,並且使 ...
前言
本節主要介紹非同步編程中Task、Async和Await的基礎知識。
什麼是非同步?
非同步處理不用阻塞當前線程來等待處理完成,而是允許後續操作,直至其它線程將處理完成,並回調通知此線程。
非同步和多線程
相同點:避免調用線程阻塞,從而提高軟體的可響應性。
不同點:
非同步操作無須額外的線程負擔,並且使用回調的方式進行處理,在設計良好的情況下,處理函數可以不必使用共用變數(即使無法完全不用,最起碼可以減少 共用變數的數量),減少了死鎖的可能。C#5.0 .NET4.5 以後關鍵字Async和Await的使用,使得非同步編程變得異常簡單。
多線程中的處理程式依然是順序執行,但是多線程的缺點也同樣明顯,線程的使用(濫用)會給系統帶來上下文切換的額外負擔。並且線程間的共用變數可能造成死鎖的出現。非同步應用場景及原理
非同步主要應用於IO操作,資料庫訪問,磁碟操作,Socket訪問、HTTP/TCP網路通信
原因:對於IO操作並不需要CPU進行過多的計算,這些數據主要通過磁碟進行處理,如果進行同步通信無法結束,需要創建更多的線程資源,線程的數據上下文頻繁的切換也是對資源的浪費,針對IO操作不需要單獨的分配一個線程來處理。
舉例說明:
操作:伺服器接收HTTP請求對資料庫進行操作然後返回
同步處理請求的線程會被阻塞,非同步處理請求的線程不會阻塞。
任務
在使用任務之前,針對線程的調用大多都用線程池提供的靜態方法QueueUserWorkItem,但是這個函數有很多的限制,其中最大的問題就是沒有內部機制可以讓開發者知道操作在什麼時候完成,也沒有機制在操作完成時獲取返回值,微軟為瞭解決這個問題引入了任務的概念。
首先構造一個Task<TResult>對象,併為TResult傳遞返回值,開始任務之後等待它並回去結果,示例代碼:
1 static void Main(string[] args) 2 { 3 Console.WriteLine("開始進行計算"); 4 // ThreadPool.QueueUserWorkItem(Sum, 10); 5 Task<int> task = new Task<int>(Sum, 100); 6 task.Start(); 7 //顯示等待獲取結果 8 task.Wait(); 9 //調用Result時,等待返回結果 10 Console.WriteLine("程式結果為 Sum = {0}",task.Result); 11 Console.WriteLine("程式結束"); 12 Console.ReadLine(); 13 } 14 15 public static int Sum(object i) 16 { 17 var sum = 0; 18 for (var j = 0; j <= (int) i; j++) 19 { 20 Console.Write("{0} + ",sum); 21 sum += j; 22 } 23 Console.WriteLine( " = {0}",sum); 24 return sum; 25 }
除了wait等待單個任務外,task還提供了等待多個任務,WaitAny和WaitAll,它阻止調用線程,直到數組中所有的Task對象完成。
取消任務
任務的取消同樣使用的是.NET Framework的標準取消操作模式,首先需要創建一個CancellationTokenSource對象,然後在函數中加入參數CancellationToken,將CancellationTokenSource的Token傳遞給方法,然後調用IsCancellationRequested獲取是否已經取消該值進行判斷。
1 static void Main(string[] args) 2 { 3 Console.WriteLine("開始進行計算"); 4 // ThreadPool.QueueUserWorkItem(Sum, 10); 5 var ctx = new CancellationTokenSource(); 6 var task = new Task<int>(() => Sum(ctx.Token, 100000)); 7 task.Start(); 8 //顯示等待獲取結果 9 //task.Wait(ctx.Token); 10 Thread.Sleep(1000); 11 ctx.Cancel(); 12 //調用Result時,等待返回結果 13 Console.WriteLine("程式結果為 Sum = {0}", task.Result); 14 Console.WriteLine("程式結束"); 15 Console.ReadLine(); 16 } 17 18 public static int Sum(CancellationToken cts, object i) 19 { 20 var sum = 0; 21 for (var j = 0; j <= (int)i; j++) 22 { 23 if (cts.IsCancellationRequested) return sum; 24 Thread.Sleep(50); 25 Console.Write("{0} + ", sum); 26 sum += j; 27 } 28 Console.WriteLine(" = {0}", sum); 29 return sum; 30 }
任務完成後自動啟動新任務
實際的開發應用中,經常出現一次任務完成後立刻啟動另外一個任務,並且不能夠使線程阻塞,在任務尚未完成時調用result會使程式阻塞,無法查看任務的執行進度,TASK提供了一個方法ContinueWith,它不會阻塞任何線程,當第一個任務完成時,會立即啟動第二個任務。
1 static void Main(string[] args) 2 { 3 Console.WriteLine("開始進行計算"); 4 // ThreadPool.QueueUserWorkItem(Sum, 10); 5 var ctx = new CancellationTokenSource(); 6 var task = new Task<int>(() => Sum(ctx.Token, 100000)); 7 task.Start(); 8 var cwt = task.ContinueWith(p => 9 { 10 Console.WriteLine("task result ={0} ",task.Result); 11 }); 12 //顯示等待獲取結果 13 //task.Wait(ctx.Token); 14 Thread.Sleep(1000); 15 ctx.Cancel(); 16 //調用Result時,等待返回結果 17 Console.WriteLine("程式結果為 Sum = {0}", task.Result); 18 Console.WriteLine("程式結束"); 19 Console.ReadLine(); 20 } 21 22 public static int Sum(CancellationToken cts, object i) 23 { 24 var sum = 0; 25 for (var j = 0; j <= (int)i; j++) 26 { 27 if (cts.IsCancellationRequested) return sum; 28 Thread.Sleep(50); 29 Console.Write("{0} + ", sum); 30 sum += j; 31 } 32 Console.WriteLine(" = {0}", sum); 33 return sum; 34 }
Async&Await 簡單使用
使用Async&Await的主要目的是方便進行非同步操作,因為.net 4.0 以前進行非同步操作時比較複雜的,主要是通過調用微軟提供的非同步回調方法進行編程,如果遇到需要自己實現的方法顯得非常頭疼,.net的各個版本都有自己主推的技術,像.NET1.1中的委托,.NET2.0中的泛型,.NET3.0中的Linq,.NET4.0中的Dynimac,.net4.5主推的就是非同步編程,大家只需要瞭解TASK+非同步函數就可以實現非同步編程。
async:告訴CLR這是一個非同步函數。
await: 將Task<TResult>返回值的函數進行非同步處理。
示例目的:獲取網址JS代碼,併在界面顯示。
1 private static async Task<string> DownloadStringWithRetries(string uri) 2 { 3 using (var client = new HttpClient()) 4 { 5 // 第1 次重試前等1 秒,第2 次等2 秒,第3 次等4 秒。 6 var nextDelay = TimeSpan.FromSeconds(1); 7 for (int i = 0; i != 3; ++i) 8 { 9 try 10 { 11 return await client.GetStringAsync(uri); 12 } 13 catch 14 { 15 } 16 await Task.Delay(nextDelay); 17 nextDelay = nextDelay + nextDelay; 18 } 19 // 最後重試一次,以便讓調用者知道出錯信息。 20 return await client.GetStringAsync(uri); 21 } 22 }
1 static void Main(string[] args) 2 { 3 Console.WriteLine("獲取百度數據"); 4 ExecuteAsync(); 5 Console.WriteLine("線程結束"); 6 Console.ReadLine(); 7 } 8 9 public static async void ExecuteAsync() 10 { 11 string text = await DownloadStringWithRetries("http://wwww.baidu.com"); 12 Console.WriteLine(text); 13 }
運行結果發現,首先獲取百度數據,線程結束,最後顯示HTML代碼,這是因為非同步開啟了新的線程,並不會造成線程阻塞。