歡迎光臨
每天分享高質量文章

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 = 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。

1、如果方法需要傳回string型別,那麼將傳回Task。如果方法沒有指定傳回型別,那麼將傳回Task。每個傳回的任務都表示正在進行的工作,任務封裝有關非同步行程狀態的資訊,如果未成功,則會引發異常。非同步方法傳回 Task 或 Task。傳回任務的屬性攜帶有關其狀態和歷史記錄的資訊,如任務是否完成、非同步方法是否導致異常或已取消以及最終結果是什麼。可使用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:

關係圖中的數值對應於以下步驟。

  1. 事件處理程式呼叫並等待 AccessTheWebAsync 非同步方法。

  2. AccessTheWebAsync 建立HttpClient實體並呼叫GetStringAsync非同步方法,獲取的內容字串方式傳回。

  3. GetStringAsync 中發生了某種情況,該情況掛起了它的行程。可能必須等待其他阻止任務完成。為避免阻止資源,GetStringAsync 會將控制權出讓給其呼叫方 AccessTheWebAsync。 GetStringAsync 傳回Task,其中 TResult 為字串,並且 AccessTheWebAsync 將任務分配給 getStringTask 變數。該任務將呼叫GetStringAsync正在進行的行程,在呼叫完成時產生傳回字串給urlcontent。

  4. 由於尚未等待 getStringTask,因此,AccessTheWebAsync 可以繼續執行而不依賴於 GetStringAsync 最終結果的完成。該任務繼續呼叫同步方法 DoIndependentWork

  5. DoIndependentWork 作為一個同步方法,在自身工作完成後傳回到呼叫方。

  6. AccessTheWebAsync 已執行完畢,可以不受 getStringTask 的結果影響。接下來,AccessTheWebAsync 需要計算並傳回已下載的字串的長度,但該方法只有在獲得字串的情況下才能計算該值。

    因此,AccessTheWebAsync 使用一個 await 運運算元來掛起其任務,並把控制權交給呼叫 AccessTheWebAsync 的事件處理程式。 AccessTheWebAsync 將 Task傳回給呼叫方。該任務將計算下載字串長度。

  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操作比較密集的業務場景下,比如查詢資料庫和網路呼叫。

贊(0)

分享創造快樂