天天看點

C#并發程式設計之異步程式設計(二)

寫在前面

前面一篇文章介紹了異步程式設計的基本内容,同時也簡要說明了async和await的一些用法。本篇文章将對async和await這兩個關鍵字進行深入探讨,研究其中的運作機制,實作編碼效率與運作效率的提升。

異步方法描述:使用async修飾符來辨別一個方法或Lambda表達式的,被稱之為異步方法。

異步方法編譯:編譯器在遇到await表達式後會截斷方法,并将剩餘的異步方法注冊為在等待任務完成後需要繼續執行的後續部分。

異步方法基礎及其運作流程

Async和Await

異步方法使用async修飾,該方法包含一個或多個await表達式或語句,方法同步運作,直至到達第一個

Await

,此時暫停,直到等待的任務完成,在任務完成後,控制權傳回給方法的調用方。如果方法中并不包含await,則該方法不會像同步方法一樣被挂起。

異步方法通常包含await運算符的一個或多個執行個體,但缺少await表達式也不會導緻生成編譯器錯誤,之會因為沒有await而發出警告,但編譯依然通過。

異步方法使用await關鍵字來确定等待位置,但await表達式并不阻止正在執行到此位置的線程,也就是說異步方法在await表達式執行時隻是暫停,并不會導緻方法退出,隻會導緻finally代碼塊不運作。異步方法隻有在等待的任務完成後,才能通過該位置并繼續執行剩下的邏輯,控制權也在此處傳回給異步方法的調用方。

如果異步方法未使用Await運算符标記暫停點,那麼異步方法會作為同步方法執行,即使有Async修飾符,也不例外。如以下示例

1:  public async static Task<string> GetUserInfoAsync()      
2:  {      
3:      User user = await db.User.FirstOrDefaultAsync();//此處會挂起      
4:         
5:      Task<User> user = db.User.FirstOrDefaultAsync();//此處不會挂起,注意此處,傳回值也變了,接下來會讨論一下異步方法的傳回值      
6:         
7:      return string.Empty;      
8:  }
       

具MSDN描述,aysnc關鍵字是一個非保留的關鍵字。 在修飾方法或 lambda 表達式時,它是關鍵字,await也作為關鍵字存在。 在所有其他上下文中,async和await都會将其解釋為辨別符。不過開發人員可以不用太過關注這段,隻需要知道aysnc會将一個方法辨別成異步方法,而await可以挂起異步方法的執行即可。

關鍵點

1、和被async修飾的方法不一樣,如果方法中含有await關鍵字,方法必須使用async辨別符,否則編譯不通過。

2、在異步程式設計過程中,比較推薦的做法是,被标記了async關鍵字的異步方法應該包含至少一個await表達式或語句。

3、異步方法的命名以Async結尾 

異步傳回類型和異常處理

需要說明的是,本文所讨論的異步方法指的是基于任務的異步程式設計模型,傳回值是,Task或Task<TResult>。

1、如果方法需要傳回string類型,那麼将傳回Task<string>。如果方法沒有指定傳回類型,那麼将傳回Task。每個傳回的任務都表示正在進行的工作,任務封裝有關異步程序狀态的資訊,如果未成功,則會引發異常。異步方法傳回 Task 或 Task<TResult>。 傳回任務的屬性攜帶有關其狀态和曆史記錄的資訊,如任務是否完成、異步方法是否導緻異常或已取消以及最終結果是什麼。 可使用await運算符通路這些屬性。

1:  public async static Task<User> GetUserInfoAsync()      
2:  {      
3:      User user = await db.User.FirstOrDefautAsync();      
4:         
5:      return user;      
6:  }      

2、如果等待的任務傳回異步方法導緻異常,則 await 運算符會以同步方式抛出異常。如果等待的傳回任務的異步方法取消,await運算符引發OperationCanceledException。如果異步方法中沒有使用await阻塞,可以使用try-catch捕捉異常,隻是異常發生的時機可能會滞後。

異步方法的運作流程

了解異步方法的運作機制,就是要了解異步程式設計中的控制流是如何一步步執行的。如果需要詳細了解控制流,可以異步到MSDN中檢視。

下圖及其描述摘自MSDN:

C#并發程式設計之異步程式設計(二)
關系圖中的數值對應于以下步驟。
  1. 事件處理程式調用并等待

    AccessTheWebAsync

    異步方法。
  2. AccessTheWebAsync

    建立HttpClient執行個體并調用GetStringAsync異步方法,擷取的内容字元串方式傳回。
  3. GetStringAsync

    中發生了某種情況,該情況挂起了它的程序。 可能必須等待其他阻止任務完成。 為避免阻止資源,

    GetStringAsync

    會将控制權出讓給其調用方

    AccessTheWebAsync

    GetStringAsync

    傳回Task<TResult>,其中 TResult 為字元串,并且

    AccessTheWebAsync

    将任務配置設定給

    getStringTask

    變量。 該任務将調用

    GetStringAsync

    正在進行的程序,在調用完成時産生傳回字元串給urlcontent。
  4. 由于尚未等待

    getStringTask

    ,是以,

    AccessTheWebAsync

    可以繼續執行而不依賴于

    GetStringAsync

    最終結果的完成。 該任務繼續調用同步方法

    DoIndependentWork

  5. DoIndependentWork

    作為一個同步方法,在自身工作完成後傳回到調用方。
  6. AccessTheWebAsync

    已運作完畢,可以不受

    getStringTask

    的結果影響。 接下來,

    AccessTheWebAsync

    需要計算并傳回已下載下傳的字元串的長度,但該方法隻有在獲得字元串的情況下才能計算該值。

    是以,

    AccessTheWebAsync

    使用一個 await 運算符來挂起其任務,并把控制權交給調用

    AccessTheWebAsync

    的事件處理程式。

    AccessTheWebAsync

    Task<int>

    傳回給調用方。 該任務将計算下載下傳字元串長度。 
  7. GetStringAsync

    完成并生成一個字元串結果。 字元串結果不是通過按你預期的方式調用

    GetStringAsync

    所傳回的。 (記住,該方法已傳回步驟 3 中的一個任務)。相反,字元串結果存儲在表示

    getStringTask

    方法完成的任務中。 await 運算符從

    getStringTask

    中檢索結果。 指派語句将檢索到的結果賦給

    urlContents

  8. AccessTheWebAsync

    擷取字元串結果時,該方法可以計算字元串長度。 然後,

    AccessTheWebAsync

    工作也将完成,并且等待事件處理程式的繼續使用。 事件處理程式也将最終獲得字元串的長度資訊。

注意:

如果 GetStringAsync(是以 getStringTask)在 AccessTheWebAsync 等待前完成,則控制權會保留在 AccessTheWebAsync中。 如果異步調用過程 (AccessTheWebAsync) 已完成,并且 AccessTheWebSync 不必等待最終結果,則挂起然後傳回到 getStringTask 将造成資源浪費。

在調用方内部(此示例中的事件處理程式),處理模式将繼續。 在等待結果前,調用方可以開展不依賴于 AccessTheWebAsync 結果的其他工作,否則就需等待片刻。 事件處理程式等待 AccessTheWebAsync,而 AccessTheWebAsync 等待 GetStringAsync。

異步程式設計對性能的影響

 在.NET異步程式設計中,async和await不會建立其他線程,同時異步方法不會在其自身線程上運作,是以它不需要多線程。隻有當方法處于活動狀态時,該方法将在目前同步上下文中運作并使用線程上的時間。可以使用Task.Run将占用大量CPU的工作移到背景線程,但是背景線程不會幫助正在等待結果的程序變為可用狀态。

對于異步程式設計而言,基于異步的方法優于幾乎每個用例中的現有方法。具體而言,這種方法優于BackgroundWorker的I/O綁定操作因為代碼更簡單且無需防止争用條件。結合Task.Run使用時,異步程式設計比BackgroundWorker更适用于CPU綁定的操作,因為異步程式設計将運作代碼的協調細節與Task.Run傳輸至線程池的工作區分開來。

那麼異步程式設計對線程的影響又是什麼呢,相比大家應該都知道,ASP.NET中有兩類線程,工作線程,和IO線程。

其中工作線程處理普通請求的線程,也是我們用得最多的線程。這個線程是有限的,是根CPU的個數相關的。IO線程,比如與檔案讀寫,網絡操作等是可以異步實作并且使性能提升的地方。I/O線程通常情況下是空閑的。是以可以使用IO線程來代替工作線程,一方面充分運用了系統資源,另一方面也節省了工作線程排程及切換所帶來的損耗。

由此我們需要明白,在I/O密集型處理時,使用異步可以帶來很大的提升,比如資料庫操作以及網絡操作。

即便異步程式設計帶來性能的提升,但是運用不慎,也會對系統性能産生反作用,比如直接使用Task.Run或者Task.Factory.StartNew所帶來的異步程式設計,這些方式會占用工作線程以及工作線程之間的切換。 

異步程式設計需要注意的地方

1、同時async和await侵入性或者傳遞性很強,所有調用的地方都需要同步使用async和await,這對系統中老代碼的修改産生了很大的影響。

2、異步程式設計中無法使用lock鎖,因為異步方法不會在自身線程上運作,lock就變成了多餘的了。但異步程式設計場景下可以使用AsyncLock鎖,對相應的代碼進行鎖定。

3、異步程式設計裡,比較推薦的做法是避免上線文延續,此處不再做更多說明,參考我的前一篇文章《異步程式設計(一)》

4、異步程式設計是否真的提升了系統性能,目前來看大多數場景下是提升了,尤其在I/O操作比較密集的業務場景下,比如查詢資料庫和網絡調用。

  • 以上為本篇文章的主要内容,希望大家多提意見,如果喜歡記得點個推薦哦

    作者:

    艾心

    出處:

    https://www.cnblogs.com/edison0621/

    本文版權歸作者和部落格園共有,歡迎轉載,轉載時保留原作者和文章位址即可。