天天看點

C#異步程式設計筆記

0x00 異步程式設計模式的曆史

.NET Framework 提供了執行異步操作的三種模式:

  • 異步程式設計模型 (APM) 模式(即 IAsyncResult 模式),在該模式下,異步操作需要使用 

    Begin

     和 

    End

     方法(例如,異步寫入操作需要使用 

    BeginWrite

     和 

    EndWrite

     方法) 不建議新的開發使用此模式。 有關詳細資訊,請參閱異步程式設計模型 (APM)。
  • 基于事件的異步模式 (EAP),這種模式需要 

    Async

     字尾,也需要一個或多個事件、事件處理程式委托類型和 

    EventArg

     派生類型。 EAP 是在 .NET Framework 2.0 中引入的。 不建議新的開發使用這種模式。 有關詳細資訊,請參閱基于事件的異步模式 (EAP)。
  • 基于任務的異步模式 (TAP),該模式使用單一方法表示異步操作的開始和完成。 TAP 是在 .NET Framework 4 中引入的,并且它是在 .NET Framework 中進行異步程式設計的推薦使用方法。 C# 中的 async 和 await 關鍵詞以及 Visual Basic 語言中的 Async 和 Await 運算符為 TAP 添加了語言支援。 有關詳細資訊,請參閱基于任務的異步模式 (TAP)。

現在主要使用TAP來程式設計。

0x01 Task和 Task<T>

任務是用于實作稱之為并發 Promise 模型的構造。 簡單地說,它們“承諾”,會在稍後完成工作,讓你使用幹淨的 API 與 promise 協作。

  • Task

     表示不傳回值的單個操作。
  • Task<T>

     表示傳回 

    T

     類型的值的單個操作。

請務必将任務了解為工作的異步抽象,而非線上程之上的抽象。 預設情況下,任務在目前線程上執行,且在适當時會将工作委托給作業系統。 可選擇性地通過 

Task.Run

 API 顯式請求任務在獨立線程上運作。

任務會公開一個 API 協定來監視、等候和通路任務的結果值(如 

Task<T>

)。 含有 

await

關鍵字的語言內建可提供進階别抽象來使用任務。

任務運作時,使用 

await

 在任務完成前将控制讓步于其調用方,可讓應用程式和服務執行有用工作。 任務完成後代碼無需依靠回調或事件便可繼續執行。 語言和任務 API 內建會為你完成此操作。 如果正在使用 

Task<T>

,任務完成時,

await

 關鍵字還将“打開”傳回的值。下面進一步詳細介紹了此工作原理。

0x02 針對 I/O 的操作的Task

以下部分介紹了使用典型異步 I/O 調用時會出現的各種情況。 我們先看兩個例子。

第一個示例調用異步方法,并傳回活動任務,很可能尚未完成。

C#複制

第二個示例還使用了 

async

 和 

await

 關鍵字對任務進行操作。

C#複制

對 

GetStringAsync()

 的調用通過低級别 .NET 庫進行(可能是調用其他異步方法),直到其到達 P/Invoke 互操作調用,進入本機網絡庫。 本機庫随後可能會調入系統 API 調用(例如 Linux 上套接字的 

write()

)。 可能會使用 TaskCompletionSource 在本機/托管邊界建立一個任務對象。 将通過層向上傳遞任務對象,對其進行操作或直接傳回,最後傳回到初始調用方。

在上述的第二個示例中,

Task<T>

 對象将直接從 

GetStringAsync

 傳回。 由于使用了 

await

 關鍵字,是以該方法會傳回一個建立的任務對象。 控制會從 

GetFirstCharactersCountAsync

 方法中的該位置傳回到調用方。 Task<T> 對象的方法和屬性確定調用方監視任務進度,當執行完 GetFirstCharactersCountAsync 中剩餘的代碼時,任務便完成。

調用系統 API 後,請求位于核心空間,一路來到作業系統的網絡子系統(例如 Linux 核心中的 

/net

)。 此處作業系統将對網絡請求進行異步處理。 所用作業系統不同,細節可能有所不同(可能會将裝置驅動程式調用安排為發送回運作時的信号,或者會執行裝置驅動程式調用然後有一個信号發送回來),但最終都會通知運作時網絡請求正在進行中。 此時,裝置驅動程式工作處于已計劃、正在進行或是已完成(請求已“通過網絡”發出),但由于這些均為異步進行,裝置驅動程式可立即着手處理其他事項!

例如,在 Windows 中作業系統線程調用網絡裝置驅動程式并要求它通過表示操作的中斷請求資料包 (IRP) 執行網絡操作。 裝置驅動程式接收 IRP,調用網絡,将 IRP 标記為“待定”,并傳回到作業系統。 由于現在作業系統線程了解到 IRP 為“待定”,是以無需再為此作業進行進一步操作,将其“傳回”,這樣它就可用于完成其他工作。

請求完成且資料通過裝置驅動程式傳回後,會經由中斷通知 CPU 新接收到的資料。 進行中斷的方式因作業系統不同而有所不同,但最終都會通過作業系統将資料傳遞到系統互操作調用(例如,Linux 中的中斷處理程式将安排 IRQ 的下半部分通過作業系統異步向上傳遞資料)。 請注意這仍是異步進行的! 在下一個可用線程能執行異步方法且“打開”已完成任務的結果前,結果會排隊等候。

在整個過程中,關鍵點在于沒有線程專用于運作任務。 盡管需要在一些上下文中執行工作(即,作業系統确實必須将資料傳遞到裝置驅動程式并響應中斷),但沒有專用于等待資料從請求傳回的線程。 這讓系統能處理更多的工作而不是等待某些 I/O 調用結束。

這對伺服器方案而言意味着什麼?

此模型可很好地處理典型的伺服器方案工作負荷。 由于沒有專用于阻止未完成任務的線程,伺服器線程池可服務更多的 Web 請求。相比伺服器将線程專用于接收到的每個請求,使用 

async

 和 

await

 能夠使伺服器多處理一個數量級的請求。

這對用戶端方案而言意味着什麼?

使用 

async

 和 

await

 對用戶端應用帶來的最大好處在于提高了響應能力。 盡管可以手動生成線程讓應用響應,但相比僅使用 

async

 和 

await

,生成線程的操作更加昂貴。 特别是對于手機遊戲等應用而言,在涉及 I/O 時盡可能少地影響 UI 線程,這點至關重要。

更重要的是,由于綁定 I/O 的工作在 CPU 上幾乎沒有耗時,是以将整個 CPU 線程專用于執行幾乎沒有任何作用的工作将是一種資源浪費。

此外,使用 

async

 方法将工作排程到 UI 線程(例如,更新 UI)十分簡單,且無需額外的工作(例如調用線程安全的委托)。

0x03 針對 CPU 的操作的Task

綁定 CPU 的 

async

 代碼與綁定 I/O 的 

async

 代碼有些許不同。 由于工作在 CPU 上執行,無法解決線程專用于計算的問題。 

async

 和 

await

 的運用使得可以與背景線程互動并讓異步方法調用方可響應。 請注意這不會為共享資料提供任何保護。 如果正在使用共享資料,仍需要采用合适的同步政策。

這裡詳細介紹了綁定 CPU 的異步調用的方方面面:

C#複制

CalculateResult()

 在調用它的線程上執行。 調用 

Task.Run

 時,它會線上程池上對昂貴的綁定 CPU 的操作 

DoExpensiveCalculation()

 進行排隊,并收到一個 

Task<int>

 句柄。

DoExpensiveCalculation()

 最終在下一個可用線程上并行運作(很可能在另一個 CPU 核心上)。 當 

DoExpensiveCalculation()

 在另一線程處理任務時,由于調用 

CalculateResult()

 的線程仍在執行,這時可能會出現并行工作的情況。

一旦遇到 

await

CalculateResult()

 執行會讓步于調用方,在 

DoExpensiveCalculation()

 執行運算的同時,允許其他任務在目前線程執行。

DoExpensiveCalculation()

 完成後,結果會在主線程上排隊等待運作。 最後,主線程将傳回執行得到 

DoExpensiveCalculation()

 結果的 

CalculateResult()

異步為什麼在此處會起作用?

async

 和 

await

 是在需要可響應性時管理綁定 CPU 的工作的最佳實踐。 存在多個可将異步用于綁定 CPU 的工作的模式。 請務必注意,使用異步成本有少許費用,不推薦緊湊循環使用它。 如何編寫此新功能的代碼完全取決于你。

0x04 異步方法的運作機制

異步程式設計中最需弄清的是控制流是如何從方法移動到方法的。 下圖可引導你完成該過程。

C#異步程式設計筆記

關系圖中的數值對應于以下步驟。

    1. 事件處理程式調用并等待 

      AccessTheWebAsync

       異步方法。
    2. AccessTheWebAsync

       可建立 HttpClient 執行個體并調用 GetStringAsync 異步方法以下載下傳網站内容作為字元串。
    3. GetStringAsync

       中發生了某種情況,該情況挂起了它的程序。 可能必須等待網站下載下傳或一些其他阻止活動。 為避免阻止資源,

      GetStringAsync

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

      AccessTheWebAsync

      GetStringAsync

       傳回 Task<TResult>,其中 

      TResult

       為字元串,并且 

      AccessTheWebAsync

       将任務配置設定給 

      getStringTask

       變量。 該任務表示調用 

      GetStringAsync

       的正在進行的程序,其中承諾當工作完成時産生實際字元串值。
    4. 由于尚未等待 

      getStringTask

      ,是以,

      AccessTheWebAsync

       可以繼續執行不依賴于 

      GetStringAsync

       得出的最終結果的其他工作。 該任務由對同步方法 

      DoIndependentWork

       的調用表示。
    5. DoIndependentWork

       是完成其工作并傳回其調用方的同步方法。
    6. AccessTheWebAsync

       已用完工作,可以不受 

      getStringTask

       的結果影響。 接下來,

      AccessTheWebAsync

       需要計算并傳回該下載下傳字元串的長度,但該方法僅在具有字元串時才能計算該值。

      是以,

      AccessTheWebAsync

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

      AccessTheWebAsync

       的方法。 

      AccessTheWebAsync

       将 

      Task<int>

       傳回給調用方。 該任務表示對産生下載下傳字元串長度的整數結果的一個承諾。

      備注

      如果 

      GetStringAsync

      (是以 

      getStringTask

      )在 

      AccessTheWebAsync

       等待前完成,則控制會保留在 

      AccessTheWebAsync

       中。 如果異步調用過程 (

      getStringTask

      ) 已完成,并且 

      AccessTheWebSync

       不必等待最終結果,則挂起然後傳回到 

      AccessTheWebAsync

       将造成成本浪費。

      在調用方内部(此示例中的事件處理程式),處理模式将繼續。 在等待結果前,調用方可以開展不依賴于 

      AccessTheWebAsync

       結果的其他工作,否則就需等待片刻。 事件處理程式等待 

      AccessTheWebAsync

      ,而 

      AccessTheWebAsync

       等待 

      GetStringAsync

    7. GetStringAsync

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

      GetStringAsync

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

      getStringTask

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

      getStringTask

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

      urlContents

    8. 當 

      AccessTheWebAsync

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

      AccessTheWebAsync

       工作也将完成,并且等待事件處理程式可繼續使用。 在此主題結尾處的完整示例中,可确認事件處理程式檢索并列印長度結果的值。

      如果你不熟悉異步程式設計,請花 1 分鐘時間考慮同步行為和異步行為之間的差異。 當其工作完成時(第 5 步)會傳回一個同步方法,但當其工作挂起時(第 3 步和第 6 步),異步方法會傳回一個任務值。 在異步方法最終完成其工作時,任務會标記為已完成,而結果(如果有)将存儲在任務中。

0x05 命名約定

按照約定,将“Async”追加到包含 

async

 修飾符的方法的名稱中。

如果某一約定中的事件、基類或接口協定建議其他名稱,則可以忽略此約定。 例如,你不應重命名常用事件處理程式,例如 

Button1_Click

轉載于:https://www.cnblogs.com/sakaiPeng/p/9764453.html

繼續閱讀