今天週五,早上起牀晚了。趕着擠公交上班。可是目前眼前有這麼幾件事情。刷牙洗臉、泡牛奶、煎蛋。在同步編程眼中。先刷牙洗臉,而後燒水泡牛奶。再煎蛋,最後喝牛奶吃蛋。毫無疑問,在時間緊促的當下。它完了,穩的遲到、半天工資沒了。那麼異步編程眼中,或許還有一絲解救的但願。先燒水,同時刷牙洗臉。而後泡牛奶,等牛奶不那麼燙的時候煎個蛋。最後喝牛奶吃蛋。也許還能不遲到。在本篇文章中將圍繞這個事例講解異步編程。html
在看異步模式以前咱們先看一個同步調用的事例:編程
class Program { private const string url = "http://www.cninnovation.com/"; static void Main(string[] args) { AsyncTest(); } public static void AsyncTest() { Console.WriteLine(nameof(AsyncTest)); using (var client=new WebClient()) { string content = client.DownloadString(url); Console.WriteLine(content.Substring(0,100)); } Console.WriteLine(); } }
在這個事例中,DownloadString方法將請求的地址下載爲string資源,可是在咱們實際運行當中,由於DownloadString方法阻塞調用線程,直到返回結果。整個程序就一直卡在了DownloadString方法這裏。這樣的體驗是很是的不愉快的。有了問題,天然也就有了對應的解決方法,下面咱們就一塊兒來看看對應的解決方法的進步史吧。c#
異步模式是處理異步特性的第一種方式,它不只可使用幾個API,還可使用基本功能(如委託類型)。不過這裏須要注意的是在使用.NET Core調用委託的這些方法時,會拋出一個異常,其中包含平臺不支持的信息。數組
異步模式定義了BeginXXX方法和EndXXX方法。例如上面同步方法是DownloadString,那麼異步就是BeginDownloadString和EndDownloadString方法。BeginXXX方法接收其同步方法的全部輸入的參數,EndXXX方法使用同步方法全部的輸出參數,並按照同步方法的返回類型來返回結果。BeginXXX定義了一個AsyncCallback參數,用於接受在異步方法執行完成後調用的委託。BeginXXX方法返回IAsyncResult,用於驗證調用是否已經完成,而且一直等到方法執行結束。服務器
咱們看下異步模式的事例,由於上面事例中的WebClient沒有異步模式的實現,這裏咱們使用WebRequest來代替:網絡
class Program { private const string url = "http://www.cninnovation.com/"; static void Main(string[] args) { AsyncTest(); } public static void AsyncTest() { Console.WriteLine(nameof(AsyncTest)); WebRequest request = WebRequest.Create(url); IAsyncResult result = request.BeginGetResponse(ReadResponse, null); Console.ReadLine(); void ReadResponse(IAsyncResult ar) { using (WebResponse response = request.EndGetResponse(ar)) { Stream stream = response.GetResponseStream(); var reader = new StreamReader(stream); string content = reader.ReadToEnd(); Console.WriteLine(content.Substring(0, 100)); Console.WriteLine(); } } } }
上面事例中展示了異步調用的一種方式---使用異步模式。先使用WebRequest類的Create方法建立WebRequest,而後使用BeginGetResponse方法異步將請求發送到服務器。調用線程沒有被阻塞。第一個參數上面有講,完成後回調的委託。一旦網絡請求完成,就會調用該方法。框架
在UI應用程序中使用異步模式有一個問題:回調的委託方法沒有在UI線程中容許,所以若是不切換到UI,就不能訪問UI元素的成員,而是拋出一個異常。調用線程不能訪問這個對象,由於另外一個線程擁有它。爲了簡化這個過程在.NET Framework 2.0 中引入了基於時間的異步模式,這樣更好的解決了此問題,下面就介紹基於事件的異步模式。異步
基於事件的異步模式定義了一個帶有」Async」後綴的方法。下面看下如何使用這個基於事件的異步模式,仍是使用的第一個事例進行修改。async
class Program { private const string url = "http://www.cninnovation.com/"; static void Main(string[] args) { AsyncTest(); } public static void AsyncTest() { Console.WriteLine(nameof(AsyncTest)); using (var client =new WebClient()) { client.DownloadStringCompleted += (sender, e) => { Console.WriteLine(e.Result.Substring(0,100)); }; client.DownloadStringAsync(new Uri(url)); Console.ReadLine(); } } }
在上述事例中,對於同步方法DownloadString,提供了一個異步變體方法DownloadStringAsync。當請求完成時會觸發DownloadStringCompleted 事件,關於事件使用及描述前面文章已有詳細介紹了。這個事件類型一共帶有兩個參數一個是object類型,一個是DownloadStringCompletedEventArgs類型。後面個這個類型經過Result屬性返回結果字符串。異步編程
這裏使用的DownloadStringCompleted 事件,事件處理成將經過保存同步上下文的線程來調用,在應用程序中這就是UI線程,所以能夠直接訪問UI元素。這裏就是與上面那個異步模式相比更優之處。下面咱們看看基於事件的異步模式進一步的改進將是什麼樣的————基於任務的異步模式。
在.NET Framework 4.5中更新了WebClient類,也新增提供了基於任務的異步模式,該模式也定義了一個」Async」後綴的方法,返回一個Task類型,可是因爲基於事件的異步模式已經採用了,因此更改成——DownloadStringTaskAsync。
DownloadStringTaskAsync方法聲明返回爲Task<string>,可是不須要一個Task<string>類型的變量接收返回結果,只須要聲明一個string類型的變量。而且使用await關鍵字。此關鍵字會解除線程的阻塞,去完成其餘的任務。咱們看下面這個事例
class Program { private const string url = "http://www.cninnovation.com/"; static async Task Main(string[] args) { await AsyncTestTask(); } public static async Task AsyncTestTask() { Console.WriteLine("當前任務Id是:"+Thread.CurrentThread.ManagedThreadId); Console.WriteLine(nameof(AsyncTestTask)); using (var client = new WebClient()) { string content = await client.DownloadStringTaskAsync(url); Console.WriteLine("當前任務Id是:"+Thread.CurrentThread.ManagedThreadId); Console.WriteLine(content.Substring(0,100)); Console.ReadLine(); } } }
上面代碼相對於以前的就較爲簡單多了,而且也沒有阻塞,不用切換回UI線程。調用順序也和同步方法同樣。
這裏我單獨的放出了容許結果,新增了當前任務顯示,在剛進入方法時任務爲1,可是執行完成DownloadStringTaskAsync方法後,任務id變成了8,上面其餘的事例容許此代碼也都是返回任務id爲1,這也就是基於任務的異步模式的不一樣點。
async和await關鍵字編譯器功能,編譯器會用Task類建立代碼。若是不使用這兩個關鍵字,也是能夠用c#4.0Task類的方法來實現一樣的功能,雖然會麻煩點。下面咱們看下async和await這兩個關鍵字能作什麼,如何採用簡單的方式建立異步方法,如何並行調用多個異步方法等等。
這裏咱們首先建立一個觀察線程和任務的方法,來更好的觀察理解發送的變化。
public static void SeeThreadAndTask(string info) { string taskinfo = Task.CurrentId == null ? "沒任務" : "任務id是:" + Task.CurrentId; Console.WriteLine($"{info} 在線程{Thread.CurrentThread.ManagedThreadId}和{taskinfo}中執行"); }
同時準備了一個同步方法,該方法使用Delay方法等待一段時間後返回一個字符串。
static void Main(string[] args) { var name= GetString("張三"); Console.WriteLine(name); } static string GetString(string name) { SeeThreadAndTask($"運行{nameof(GetString)}"); Task.Delay(3000).Wait(); return $"你好,{name}"; }
上面咱們也說了不使用哪兩個關鍵字也可使用Task類實現一樣的功能,這裏咱們採用一個簡單的作大,使用Task.Run方法返回一個任務。
static void Main(string[] args) { SeeThreadAndTask($"運行{nameof(Main)}"); var name= GetStringAsync("張三"); Console.WriteLine(name.Result); Console.ReadLine(); } static Task<string> GetStringAsync(string name) => Task.Run<string>(() => { SeeThreadAndTask($"運行{nameof(GetStringAsync)}"); return GetString(name); });
咱們繼續來看await和async關鍵字,使用await關鍵字調用返回任務的異步方法,可是也須要使用async修飾符。
static void Main(string[] args) { SeeThreadAndTask($"運行{nameof(Main)}"); GetSelfAsync("張三"); Console.ReadLine(); } private static async void GetSelfAsync(string name) { SeeThreadAndTask($"開始運行{nameof(GetSelfAsync)}"); string result =await GetStringAsync(name); Console.WriteLine(result); SeeThreadAndTask($"結束運行{nameof(GetSelfAsync)}"); }
在異步方法完成前,該方法內的其餘代碼不會執行。可是,啓動GetSelfAsync方法的線程能夠被重用。該線程沒有被阻塞。
這裏剛開始時候中是沒有任務執行的,GetStringAsync方法開始在一個任務中執行,這裏所在的線程也是不一樣的。其中GetString和GetStringAsync方法都執行完畢,等待以後返回如今GetStringAsync開始轉變爲線程3,同時也沒有任務。await確保任務完成後繼續執行,可是如今使用的是另外一個線程。這一個行爲在咱們使用控制檯應用程序和具備同步上下文的應用程序之間是不一樣的。
能夠對任何提供GetAwaiter方法並對awaiter的對象async關鍵字。其中awaiter用OnCompleted方法實現INotifyCompletion接口,完成任務時調用,下面事例中沒有使用await關鍵字,而是使用GetAwaiter方法,返回一個TaskAwaiter,而且使用OnCompleted方法,分配一個在任務完成時調用的本地函數。
static void Main(string[] args) { SeeThreadAndTask($"運行{nameof(Main)}"); GetSelfAwaiter("張三"); Console.ReadLine(); } private static void GetSelfAwaiter(string name) { SeeThreadAndTask($"運行{nameof(GetSelfAwaiter)}"); TaskAwaiter<string> awaiter = GetStringAsync(name).GetAwaiter(); awaiter.OnCompleted(OnCompletedAwauter); void OnCompletedAwauter() { Console.WriteLine(awaiter.GetResult()); SeeThreadAndTask($"運行{nameof(GetSelfAwaiter)}"); } }
咱們看這個運行結果,再與上面調用異步方法的運行結果進行對比,好像相似於使用await關鍵字的情形。至關於編譯器把await關鍵字後面的全部的代碼放進OnCompleted方法的代碼塊中完成。固然也可另外方法使用GetAwaiter方法。
static void Main(string[] args) { SeeThreadAndTask($"運行{nameof(Main)}"); GetSelfAwaiter("張三"); Console.ReadLine(); } private static void GetSelfAwaiter(string name) { SeeThreadAndTask($"運行{nameof(GetSelfAwaiter)}"); string awaiter = GetStringAsync(name).GetAwaiter().GetResult(); Console.WriteLine(awaiter); SeeThreadAndTask($"運行{nameof(GetSelfAwaiter)}"); }
這裏咱們介紹使用Task對象的特性來處理任務的延續。GetStringAsync方法返回一個Task<string>對象包含了任務建立的一些信息,並一直保存到任務完成。Task類的ContinueWith定義了完成任務以後就調用的代碼。這裏指派給ContinueWith方法的委託接收將已完成的任務做爲參數傳入,可使用Result屬性訪問任務的返回結果。
static void Main(string[] args) { SeeThreadAndTask($"運行{nameof(Main)}"); GetStringContinueAsync("張三"); Console.ReadLine(); } /// <summary>
/// 使用ContinueWith延續任務 /// </summary>
/// <param name="name"></param>
private static void GetStringContinueAsync(string name) { SeeThreadAndTask($"開始 運行{nameof(GetStringContinueAsync)}"); var result = GetStringAsync(name); result.ContinueWith(t=> { string answr = t.Result; Console.WriteLine(answr); SeeThreadAndTask($"結束 運行{nameof(GetStringContinueAsync)}"); }); }
這裏咱們觀察運行結果能夠發如今執行完成任務後繼續執行ContinueWith方法。其中這個方法在線程4和任務2中完成。這裏至關於又開始了一個新的任務,也就是使用ContinueWith方法對任務進行必定的延續。
在每一個異步方法中能夠調用一個或多個異步方法。那麼如何進行編碼呢?這就看這些異步方法之間是否存在相互依賴了。
正常來講按照順序調用:
static void Main(string[] args) { SeeThreadAndTask($"運行{nameof(Main)}"); ManyAsyncFun(); Console.ReadLine(); } private static async void ManyAsyncFun() { var result1 = await GetStringAsync("張三"); var result2 = await GetStringAsync("李四"); Console.WriteLine($"第一我的是{result1},第二我的是{result2}"); }
使用await關鍵字調用每一個異步方法。若是一個異步方法依賴另外一個異步方法的話,那麼這個await關鍵字就比較有效,可是若是第二個異步方法獨立於第一個異步方法,這樣能夠不使用await關鍵字,這樣的話整個ManyAsyncFun方法將會更快的返回結果。
還一種狀況,異步方法不依賴於其餘異步方法,並且不使用await,而是把每一個異步方法的返回結果賦值給Task比變量,這樣會運行的更快。組合器能夠幫助實現這一點,一個組合器能夠接受多個同一類型的參數,並返回同一類型的值。若是任務返回相同的類型,那麼該類型的數組也可用於接收await返回的結果。當只有等待全部任務都完成時才能繼續完成其餘的任務時,WhenAll方法就有實際用途,當調用的任務在等待完成時任何任務都能繼續完成任務的時候就能夠採用WhenAny方法,它可使用任務的結果繼續。
static void Main(string[] args) { SeeThreadAndTask($"運行{nameof(Main)}"); ManyAsyncFunWithWhenAll(); Console.ReadLine(); } private static async void ManyAsyncFunWithWhenAll() { Task<string> result1 = GetStringAsync("張三"); Task<string> result2 = GetStringAsync("李四"); await Task.WhenAll(result1, result2); Console.WriteLine($"第一我的是{result1.Result},第二我的是{result2.Result}"); }
在使用await依次調用兩個異步方法時,診斷會話6.646秒,採用WhenAll時,診斷會話話費3.912秒,能夠看出速度明顯提升了。
C#帶有更靈活的await關鍵字:它如今能夠等待任何提供GetAwaiter方法的對象。下面咱們講一個可用於等待的新類型-----ValueTask,與Task相反,ValueTask是一個結構。這具備性能優點,因ValueTask在堆上沒有對象。
static async Task Main(string[] args) { SeeThreadAndTask($"運行{nameof(Main)}"); for (int i = 0; i < 10000; i++) { string result2 = await GetStringDicAsync("張三"); } Console.WriteLine("結束"); Console.ReadLine(); } private readonly static Dictionary<string, string> names = new Dictionary<string, string>(); private static async ValueTask<string> GetStringDicAsync(string name) { if (names.TryGetValue(name,out string result)) { return result; } else { result = await GetStringAsync(name); names.Add(name,result); return result; } }
上面事例中咱們使用ValueTask替代了Task,由於咱們前面講,每次使用Task都會對內存進行分配空間,在咱們反覆時會形成必定的性能上的損耗,可是使用ValueTask只會存放在Stack中,存放實際值而不是記憶地址。
並不是全部的.NET Framework的全部的類都引用了新的異步方法,在使用框架中不一樣的類的時候會發現,還有許多類只提供了BeginXXX方法和EndXXX方法的異步模式,沒有提供基於任務的異步模式,可是咱們能夠把異步模式更改成基於任務的異步模式。
提供的Task.Factory.FromAsync<>泛型方法,將異步模式轉換爲基於任務的異步模式。
static void Main(string[] args) { ConvertingAsync(); Console.ReadLine(); } private static async void ConvertingAsync() { HttpWebRequest request = WebRequest.Create("http://www.cninnovation.com/") as HttpWebRequest; using (WebResponse response = await Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse(null,null),request.EndGetResponse)) { Stream stream = response.GetResponseStream(); using (var reader=new StreamReader(stream)) { string content = reader.ReadToEnd(); Console.WriteLine(content.Substring(0,100)); } } }
上一節咱們講了錯誤和異常處理,可是咱們在使用異步方法時,應該知道一些特殊的處理方式,咱們先看一個簡單的事例
static void Main(string[] args) { Dont(); Console.WriteLine("結束"); Console.ReadLine(); } static async Task ThrowAfterAsync(int ms, string msg) { await Task.Delay(ms); throw new Exception(msg); } private static void Dont() { try { ThrowAfterAsync(200,"第一個錯誤"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
在這個事例中,調用了異步方法,可是並無等待,try/catch就捕獲不到異常,這是由於Dont方法在拋出異常前就運行結束了。
那麼異步方法的異常怎麼處理呢,有一個較好的方法就是使用await關鍵字。將其放在try/catch中,異步方法調用完後,Dont方法就會釋放線程,但它會在任務完成時保持任務的引用。
private static async void Dont() { try { await ThrowAfterAsync(200,"第一個錯誤"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
那麼多個異步方法調用,每一個都拋出異常怎麼處理呢?咱們看下面事例中
private static async void Dont() { try { await ThrowAfterAsync(200,"第一個錯誤"); await ThrowAfterAsync(100, "第二個錯誤"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
調用兩個異步方法,可是都拋出異常,由於捕獲了一個異常以後,try塊代碼就沒有繼續調用第二方法,也就只拋出了第一個異常
private static async void Dont() { try { Task t1 = ThrowAfterAsync(200, "第一個錯誤"); Task t2 = ThrowAfterAsync(100, "第二個錯誤"); await Task.WhenAll(t1,t2); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
對上述事例修改,採用並行調用兩個方法,在2s秒後第一個拋出異常,1s秒後第二個異常也拋出了,使用Task.WhenAll,不論是否拋出異常,都會等兩個任務完成。所以就算捕獲了第一個異常也會執行第二個方法。可是咱們只能看見拋出的第一個異常,沒有顯示第二個異常,可是它存在在列表中。
這裏爲了獲得全部失敗任務的異常信息,看將Task.WhenAll返回的結果寫到一個Task變量中。這個任務會一個等到全部任務結束。
private static async void Dont() { Task taskResult = null; try { Task t1 = ThrowAfterAsync(200, "第一個錯誤"); Task t2 = ThrowAfterAsync(100, "第二個錯誤"); await (taskResult=Task.WhenAll(t1,t2)); } catch (Exception ex) { Console.WriteLine(ex.Message); foreach (var item in taskResult.Exception.InnerExceptions) { Console.WriteLine(item.Message); } } }
這裏能夠訪問外部任務的Exception屬性了。Exception屬性是AggregateException類型的。這裏使用Task.Exception.InnerExceptions屬性,它包含了等待中全部的異常列表。這樣就能夠輕鬆的變量全部的異常了。
本篇文章介紹了三種不一樣的異步模式,同時也介紹 了相關的異步編程基礎。如何對應的去使用異步方法大有學問,用的好的異步編程減小性能消耗,提升運行效率。可是使用很差的異步編程提升性能消耗,下降運行效率也不是不可能的。這裏也只是簡單的介紹了異步編程的相關基礎知識以及錯誤處理。更深更完美的編程模式還得實踐中去探索。異步編程使用async和await關鍵字等待這些方法。而不會阻塞線程。異步編程的介紹到這裏就暫時結束,下一篇文章咱們將詳細介紹反射、元數據。
不是井裏沒有水,而是你挖的不夠深。不是成功來得慢,而是你努力的不夠多。
歡迎你們掃描下方二維碼,和我一塊兒學習更多的C#知識