.Net異步編程詳解入門


前言

  今天周五,早上起床晚了。趕着擠公交上班。但是目前眼前有這么幾件事情。刷牙洗臉、泡牛奶、煎蛋。在同步編程眼中。先刷牙洗臉,然后燒水泡牛奶。再煎蛋,最后喝牛奶吃蛋。毫無疑問,在時間緊促的當下。它完了,穩的遲到、半天工資沒了。那么異步編程眼中,或許還有一絲解救的希望。先燒水,同時刷牙洗臉。然后泡牛奶,等牛奶不那么燙的時候煎個蛋。最后喝牛奶吃蛋。也許還能不遲到。在本篇文章中將圍繞這個事例講解異步編程。

異步編程不同模式

  在看異步模式之前我們先看一個同步調用的事例:

 

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方法這里。這樣的體驗是非常的不愉快的。有了問題,自然也就有了對應的解決方法,下面我們就一起來看看對應的解決方法的進步史吧。

一、異步模式

  異步模式是處理異步特性的第一種方式,它不僅可以使用幾個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”后綴的方法。下面看下如何使用這個基於事件的異步模式,還是使用的第一個事例進行修改。

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確保任務完成后繼續執行,但是現在使用的是另一個線程。這一個行為在我們使用控制台應用程序和具有同步上下文的應用程序之間是不同的。

三、使用Awaiter

  可以對任何提供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秒,可以看出速度明顯提高了。

       

                 

 

六、使用ValueTask

  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,不管是否拋出異常,都會等兩個任務完成。因此就算捕獲了第一個異常也會執行第二個方法。但是我們只能看見拋出的第一個異常,沒有顯示第二個異常,但是它存在在列表中。

三、使用AggregateException

這里為了得到所有失敗任務的異常信息,看將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#基礎知識詳解系列

 

歡迎大家掃描下方二維碼,和我一起學習更多的C#知識 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM