C#異步編程筆記

0x00 異步編程模式的歷史

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

  • 異步編程模型 (APM) 模式(即 IAsyncResult 模式),在該模式下,異步操做須要使用 Begin 和 End 方法(例如,異步寫入操做須要使用 BeginWrite 和 EndWrite 方法) 不建議新的開發使用此模式。 有關詳細信息,請參閱異步編程模型 (APM)api

  • 基於事件的異步模式 (EAP),這種模式須要 Async 後綴,也須要一個或多個事件、事件處理程序委託類型和 EventArg 派生類型。 EAP 是在 .NET Framework 2.0 中引入的。 不建議新的開發使用這種模式。 有關詳細信息,請參閱基於事件的異步模式 (EAP)promise

  • 基於任務的異步模式 (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關鍵字的語言集成可提供高級別抽象來使用任務。app

任務運行時,使用 await 在任務完成前將控制讓步於其調用方,可以讓應用程序和服務執行有用工做。 任務完成後代碼無需依靠回調或事件即可繼續執行。 語言和任務 API 集成會爲你完成此操做。 若是正在使用 Task<T>,任務完成時,await 關鍵字還將「打開」返回的值。下面進一步詳細介紹了此工做原理。異步

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

如下部分介紹了使用典型異步 I/O 調用時會出現的各類狀況。 咱們先看兩個例子。async

第一個示例調用異步方法,並返回活動任務,極可能還沒有完成。

C#
public Task<string> GetHtmlAsync() { // Execution is synchronous here var client = new HttpClient(); return client.GetStringAsync("http://www.dotnetfoundation.org"); } 

第二個示例還使用了 async 和 await 關鍵字對任務進行操做。

C#
public async Task<string> GetFirstCharactersCountAsync(string url, int count) { // Execution is synchronous here var client = new HttpClient(); // Execution of GetFirstCharactersCountAsync() is yielded to the caller here // GetStringAsync returns a Task<string>, which is *awaited* var page = await client.GetStringAsync("http://www.dotnetfoundation.org"); // Execution resumes when the client.GetStringAsync task completes, // becoming synchronous again. if (count > page.Length) { return page; } else { return page.Substring(0, count); } } 

對 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#
public async Task<int> CalculateResult(InputData data) { // This queues up the work on the threadpool. var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data)); // Note that at this point, you can do some other work concurrently, // as CalculateResult() is still executing! // Execution of CalculateResult is yielded here! var result = await expensiveResultTask; return result; } 

CalculateResult() 在調用它的線程上執行。 調用 Task.Run 時,它會在線程池上對昂貴的綁定 CPU 的操做 DoExpensiveCalculation() 進行排隊,並收到一個 Task<int> 句柄。DoExpensiveCalculation() 最終在下一個可用線程上並行運行(極可能在另外一個 CPU 內核上)。 當 DoExpensiveCalculation() 在另外一線程處理任務時,因爲調用 CalculateResult() 的線程仍在執行,這時可能會出現並行工做的狀況。

一旦遇到 awaitCalculateResult() 執行會讓步於調用方,在 DoExpensiveCalculation() 執行運算的同時,容許其餘任務在當前線程執行。DoExpensiveCalculation() 完成後,結果會在主線程上排隊等待運行。 最後,主線程將返回執行獲得 DoExpensiveCalculation() 結果的 CalculateResult()

異步爲何在此處會起做用?

async 和 await 是在須要可響應性時管理綁定 CPU 的工做的最佳實踐。 存在多個可將異步用於綁定 CPU 的工做的模式。 請務必注意,使用異步成本有少量費用,不推薦緊湊循環使用它。 如何編寫此新功能的代碼徹底取決於你。

 

0x04 異步方法的運行機制

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

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

    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

相關文章
相關標籤/搜索