C#併發編程之異步編程(二)

寫在前面

前面一篇文章介紹了異步編程的基本內容,同時也簡要說明了async和await的一些用法。本篇文章將對async和await這兩個關鍵字進行深刻探討,研究其中的運行機制,實現編碼效率與運行效率的提高。html


異步方法描述:使用async修飾符來標識一個方法或Lambda表達式的,被稱之爲異步方法。數據庫

異步方法編譯:編譯器在遇到await表達式後會截斷方法,並將剩餘的異步方法註冊爲在等待任務完成後須要繼續執行的後續部分。編程

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

Async和Await

異步方法使用async修飾,該方法包含一個或多個await表達式或語句,方法同步運行,直至到達第一個 Await,此時暫停,直到等待的任務完成,在任務完成後,控制權返回給方法的調用方。若是方法中並不包含await,則該方法不會像同步方法同樣被掛起。網絡

異步方法一般包含await運算符的一個或多個實例,但缺乏await表達式也不會致使生成編譯器錯誤,之會由於沒有await而發出警告,但編譯依然經過。多線程

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

若是異步方法未使用Await運算符標記暫停點,那麼異步方法會做爲同步方法執行,即便有Async修飾符,也不例外。如如下示例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能夠掛起異步方法的執行便可。

關鍵點ide

一、和被async修飾的方法不同,若是方法中含有await關鍵字,方法必須使用async標識符,不然編譯不經過。異步編程

二、在異步編程過程當中,比較推薦的作法是,被標記了async關鍵字的異步方法應該包含至少一個await表達式或語句。性能

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

異步返回類型和異常處理

須要說明的是,本文所討論的異步方法指的是基於任務的異步編程模型,返回值是,Task或Task<TResult>。

一、若是方法須要返回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:  }

二、若是等待的任務返回異步方法致使異常,則 await 運算符會以同步方式拋出異常。若是等待的返回任務的異步方法取消,await運算符引起OperationCanceledException。若是異步方法中沒有使用await阻塞,可使用try-catch捕捉異常,只是異常發生的時機可能會滯後。

異步方法的運行流程

瞭解異步方法的運行機制,就是要了解異步編程中的控制流是如何一步步執行的。若是須要詳細瞭解控制流,能夠異步到MSDN中查看。

下圖及其描述摘自MSDN

navigation-trace-async-program

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

  1. 事件處理程序調用並等待 AccessTheWebAsync 異步方法。

  2. AccessTheWebAsync 建立HttpClient實例並調用GetStringAsync異步方法,獲取的內容字符串方式返回。

  3. GetStringAsync 中發生了某種狀況,該狀況掛起了它的進程。 可能必須等待其餘阻止任務完成。 爲避免阻止資源,GetStringAsync 會將控制權出讓給其調用方 AccessTheWebAsyncGetStringAsync 返回Task<TResult>,其中 TResult 爲字符串,而且 AccessTheWebAsync 將任務分配給 getStringTask 變量。 該任務將調用GetStringAsync正在進行的進程,在調用完成時產生返回字符串給urlcontent。

  4. 因爲還沒有等待 getStringTask,所以,AccessTheWebAsync 能夠繼續執行而不依賴於 GetStringAsync 最終結果的完成。 該任務繼續調用同步方法 DoIndependentWork

  5. DoIndependentWork 做爲一個同步方法,在自身工做完成後返回到調用方。

  6. AccessTheWebAsync 已運行完畢,能夠不受 getStringTask 的結果影響。 接下來,AccessTheWebAsync 須要計算並返回已下載的字符串的長度,但該方法只有在得到字符串的狀況下才能計算該值。

    所以,AccessTheWebAsync 使用一個 await 運算符來掛起其任務,並把控制權交給調用 AccessTheWebAsync 的事件處理程序。 AccessTheWebAsyncTask<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所帶來的異步編程,這些方式會佔用工做線程以及工做線程之間的切換。 

異步編程須要注意的地方


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

二、異步編程中沒法使用lock鎖,由於異步方法不會在自身線程上運行,lock就變成了多餘的了。但異步編程場景下可使用AsyncLock鎖,對相應的代碼進行鎖定。

三、異步編程裏,比較推薦的作法是避免上線文延續,此處再也不作更多說明,參考個人前一篇文章《異步編程(一)》

四、異步編程是否真的提高了系統性能,目前來看大多數場景下是提高了,尤爲在I/O操做比較密集的業務場景下,好比查詢數據庫和網絡調用。

相關文章
相關標籤/搜索