异步编程中的最佳做法(Async/Await) --转
近日來,涌現(xiàn)了許多關(guān)于 Microsoft .NET Framework 4.5 中新增了對 async 和 await 支持的信息。?本文旨在作為學(xué)習(xí)異步編程的“第二步”;我假設(shè)您已閱讀過有關(guān)這一方面的至少一篇介紹性文章。?本文不提供任何新內(nèi)容,Stack Overflow、MSDN 論壇和 async/await FAQ 這類在線資源提供了同樣的建議。?本文只重點介紹一些淹沒在文檔海洋中的最佳做法。
本文中的最佳做法更大程度上是“指導(dǎo)原則”,而不是實際規(guī)則。?其中每個指導(dǎo)原則都有一些例外情況。?我將解釋每個指導(dǎo)原則背后的原因,以便可以清楚地了解何時適用以及何時不適用。?圖 1?中總結(jié)了這些指導(dǎo)原則;我將在以下各節(jié)中逐一討論。
圖 1 異步編程指導(dǎo)原則總結(jié)
| “名稱” | 說明 | 異常 |
| 避免 Async Void | 最好使用 async Task 方法而不是 async void 方法 | 事件處理程序 |
| 始終使用 Async | 不要混合阻塞式代碼和異步代碼 | 控制臺 main 方法 |
| 配置上下文 | 盡可能使用 ConfigureAwait(false) | 需要上下文的方法 |
避免 Async Void
Async 方法有三種可能的返回類型: Task、Task<T> 和 void,但是 async 方法的固有返回類型只有 Task 和 Task<T>。?當(dāng)從同步轉(zhuǎn)換為異步代碼時,任何返回類型 T 的方法都會成為返回 Task<T> 的 async 方法,任何返回 void 的方法都會成為返回 Task 的 async 方法。?下面的代碼段演示了一個返回 void 的同步方法及其等效的異步方法:
返回 void 的 async 方法具有特定用途: 用于支持異步事件處理程序。?事件處理程序可以返回某些實際類型,但無法以相關(guān)語言正常工作;調(diào)用返回類型的事件處理程序非常困難,事件處理程序?qū)嶋H返回某些內(nèi)容這一概念也沒有太大意義。?事件處理程序本質(zhì)上返回 void,因此 async 方法返回 void,以便可以使用異步事件處理程序。?但是,async void 方法的一些語義與 async Task 或 async Task<T> 方法的語義略有不同。
Async void 方法具有不同的錯誤處理語義。?當(dāng) async Task 或 async Task<T> 方法引發(fā)異常時,會捕獲該異常并將其置于 Task 對象上。?對于 async void 方法,沒有 Task 對象,因此 async void 方法引發(fā)的任何異常都會直接在 SynchronizationContext(在 async void 方法啟動時處于活動狀態(tài))上引發(fā)。?圖 2?演示本質(zhì)上無法捕獲從 async void 方法引發(fā)的異常。
圖 2 無法使用 Catch 捕獲來自 Async Void 方法的異常
可以通過對 GUI/ASP.NET 應(yīng)用程序使用 AppDomain.UnhandledException 或類似的全部捕獲事件觀察到這些異常,但是使用這些事件進行常規(guī)異常處理會導(dǎo)致無法維護。
Async void 方法具有不同的組合語義。?返回 Task 或 Task<T> 的 async 方法可以使用 await、Task.WhenAny、Task.WhenAll 等方便地組合而成。?返回 void 的 async 方法未提供一種簡單方式,用于向調(diào)用代碼通知它們已完成。?啟動幾個 async void 方法不難,但是確定它們何時結(jié)束卻不易。?Async void 方法會在啟動和結(jié)束時通知 SynchronizationContext,但是對于常規(guī)應(yīng)用程序代碼而言,自定義 SynchronizationContext 是一種復(fù)雜的解決方案。
Async void 方法難以測試。?由于錯誤處理和組合方面的差異,因此調(diào)用 async void 方法的單元測試不易編寫。?MSTest 異步測試支持僅適用于返回 Task 或 Task<T> 的 async 方法。?可以安裝 SynchronizationContext 來檢測所有 async void 方法都已完成的時間并收集所有異常,不過只需使 async void 方法改為返回 Task,這會簡單得多。
顯然,async void 方法與 async Task 方法相比具有幾個缺點,但是這些方法在一種特定情況下十分有用: 異步事件處理程序。?語義方面的差異對于異步事件處理程序十分有意義。?它們會直接在 SynchronizationContext 上引發(fā)異常,這類似于同步事件處理程序的行為方式。?同步事件處理程序通常是私有的,因此無法組合或直接測試。?我喜歡采用的一個方法是盡量減少異步事件處理程序中的代碼(例如,讓它等待包含實際邏輯的 async Task 方法)。?下面的代碼演示了這一方法,該方法通過將 async void 方法用于事件處理程序而不犧牲可測試性:
如果調(diào)用方不希望 async void 方法是異步的,則這些方法可能會造成嚴(yán)重影響。?當(dāng)返回類型是 Task 時,調(diào)用方知道它在處理將來的操作;當(dāng)返回類型是 void 時,調(diào)用方可能假設(shè)方法在返回時完成。?此問題可能會以許多意外方式出現(xiàn)。?在接口(或基類)上提供返回 void 的方法的 async 實現(xiàn)(或重寫)通常是錯誤的。某些事件也假設(shè)其處理程序在返回時完成。?一個不易察覺的陷阱是將 async lambda 傳遞到采用 Action 參數(shù)的方法;在這種情況下,async lambda 返回 void 并繼承 async void 方法的所有問題。?一般而言,僅當(dāng) async lambda 轉(zhuǎn)換為返回 Task 的委托類型(例如,Func<Task>)時,才應(yīng)使用 async lambda。
總結(jié)這第一個指導(dǎo)原則便是,應(yīng)首選 async Task 而不是 async void。?Async Task 方法更便于實現(xiàn)錯誤處理、可組合性和可測試性。?此指導(dǎo)原則的例外情況是異步事件處理程序,這類處理程序必須返回 void。?此例外情況包括邏輯上是事件處理程序的方法,即使它們字面上不是事件處理程序(例如 ICommand.Execute implementations)。
始終使用 Async
異步代碼讓我想起了一個故事,有個人提出世界是懸浮在太空中的,但是一個老婦人立即提出質(zhì)疑,她聲稱世界位于一個巨大烏龜?shù)谋成稀?當(dāng)這個人問烏龜站在哪里時,老夫人回答:“很聰明,年輕人,下面是一連串的烏龜!”在將同步代碼轉(zhuǎn)換為異步代碼時,您會發(fā)現(xiàn),如果異步代碼調(diào)用其他異步代碼并且被其他異步代碼所調(diào)用,則效果最好 — 一路向下(或者也可以說“向上”)。?其他人已注意到異步編程的傳播行為,并將其稱為“傳染”或?qū)⑵渑c僵尸病毒進行比較。?無論是烏龜還是僵尸,無可置疑的是,異步代碼趨向于推動周圍的代碼也成為異步代碼。?此行為是所有類型的異步編程中所固有的,而不僅僅是新 async/await 關(guān)鍵字。
“始終異步”表示,在未慎重考慮后果的情況下,不應(yīng)混合使用同步和異步代碼。?具體而言,通過調(diào)用 Task.Wait 或 Task.Result 在異步代碼上進行阻塞通常很糟糕。?對于在異步編程方面“淺嘗輒止”的程序員,這是個特別常見的問題,他們僅僅轉(zhuǎn)換一小部分應(yīng)用程序,并采用同步 API 包裝它,以便代碼更改與應(yīng)用程序的其余部分隔離。?不幸的是,他們會遇到與死鎖有關(guān)的問題。?在 MSDN 論壇、Stack Overflow 和電子郵件中回答了許多與異步相關(guān)的問題之后,我可以說,迄今為止,這是異步初學(xué)者在了解基礎(chǔ)知識之后最常提問的問題: “為何我的部分異步代碼死鎖?”
圖 3?演示一個簡單示例,其中一個方法發(fā)生阻塞,等待 async 方法的結(jié)果。?此代碼僅在控制臺應(yīng)用程序中工作良好,但是在從 GUI 或 ASP.NET 上下文調(diào)用時會死鎖。?此行為可能會令人困惑,尤其是通過調(diào)試程序單步執(zhí)行時,這意味著沒完沒了的等待。?在調(diào)用 Task.Wait 時,導(dǎo)致死鎖的實際原因在調(diào)用堆棧中上移。
圖 3 在異步代碼上阻塞時的常見死鎖問題
這種死鎖的根本原因是 await 處理上下文的方式。?默認(rèn)情況下,當(dāng)?shù)却赐瓿傻?Task 時,會捕獲當(dāng)前“上下文”,在 Task 完成時使用該上下文恢復(fù)方法的執(zhí)行。?此“上下文”是當(dāng)前 SynchronizationContext(除非它是 null,這種情況下則為當(dāng)前 TaskScheduler)。?GUI 和 ASP.NET 應(yīng)用程序具有 SynchronizationContext,它每次僅允許一個代碼區(qū)塊運行。?當(dāng) await 完成時,它會嘗試在捕獲的上下文中執(zhí)行 async 方法的剩余部分。但是該上下文已含有一個線程,該線程在(同步)等待 async 方法完成。?它們相互等待對方,從而導(dǎo)致死鎖。
請注意,控制臺應(yīng)用程序不會形成這種死鎖。?它們具有線程池 SynchronizationContext 而不是每次執(zhí)行一個區(qū)塊的 SynchronizationContext,因此當(dāng) await 完成時,它會在線程池線程上安排 async 方法的剩余部分。該方法能夠完成,并完成其返回任務(wù),因此不存在死鎖。?當(dāng)程序員編寫測試控制臺程序,觀察到部分異步代碼按預(yù)期方式工作,然后將相同代碼移動到 GUI 或 ASP.NET 應(yīng)用程序中會發(fā)生死鎖,此行為差異可能會令人困惑。
此問題的最佳解決方案是允許異步代碼通過基本代碼自然擴展。?如果采用此解決方案,則會看到異步代碼擴展到其入口點(通常是事件處理程序或控制器操作)。?控制臺應(yīng)用程序不能完全采用此解決方案,因為 Main 方法不能是 async。?如果 Main 方法是 async,則可能會在完成之前返回,從而導(dǎo)致程序結(jié)束。?圖 4演示了指導(dǎo)原則的這一例外情況:?控制臺應(yīng)用程序的 Main 方法是代碼可以在異步方法上阻塞為數(shù)不多的幾種情況之一。
圖 4 Main 方法可以調(diào)用 Task.Wait 或 Task.Result
允許異步代碼通過基本代碼擴展是最佳解決方案,但是這意味著需進行許多初始工作,該應(yīng)用程序才能體現(xiàn)出異步代碼的實際好處。?可通過幾種方法逐漸將大量基本代碼轉(zhuǎn)換為異步代碼,但是這超出了本文的范圍。在某些情況下,使用 Task.Wait 或 Task.Result 可能有助于進行部分轉(zhuǎn)換,但是需要了解死鎖問題以及錯誤處理問題。?我現(xiàn)在說明錯誤處理問題,并在本文后面演示如何避免死鎖問題。
每個 Task 都會存儲一個異常列表。?等待 Task 時,會重新引發(fā)第一個異常,因此可以捕獲特定異常類型(如 InvalidOperationException)。?但是,在 Task 上使用 Task.Wait 或 Task.Result 同步阻塞時,所有異常都會用 AggregateException 包裝后引發(fā)。?請再次參閱圖 4。?MainAsync 中的 try/catch 會捕獲特定異常類型,但是如果將 try/catch 置于 Main 中,則它會始終捕獲 AggregateException。?當(dāng)沒有 AggregateException 時,錯誤處理要容易處理得多,因此我將“全局”try/catch 置于 MainAsync 中。
至此,我演示了兩個與異步代碼上阻塞有關(guān)的問題: 可能的死鎖和更復(fù)雜的錯誤處理。?對于在 async 方法中使用阻塞代碼,也有一個問題。?請考慮此簡單示例:
此方法不是完全異步的。?它會立即放棄,返回未完成的任務(wù),但是當(dāng)它恢復(fù)執(zhí)行時,會同步阻塞線程正在運行的任何內(nèi)容。?如果此方法是從 GUI 上下文調(diào)用,則它會阻塞 GUI 線程;如果是從 ASP.NET 請求上下文調(diào)用,則會阻塞當(dāng)前 ASP.NET 請求線程。?如果異步代碼不同步阻塞,則其工作效果最佳。?圖 5?是將同步操作替換為異步替換的速查表。
圖 5 執(zhí)行操作的“異步方式”
| 執(zhí)行以下操作… | 替換以下方式… | 使用以下方式 |
| 檢索后臺任務(wù)的結(jié)果 | Task.Wait 或 Task.Result | await |
| 等待任何任務(wù)完成 | Task.WaitAny | await Task.WhenAny |
| 檢索多個任務(wù)的結(jié)果 | Task.WaitAll | await Task.WhenAll |
| 等待一段時間 | Thread.Sleep | await Task.Delay |
總結(jié)這第二個指導(dǎo)原則便是,應(yīng)避免混合使用異步代碼和阻塞代碼。?混合異步代碼和阻塞代碼可能會導(dǎo)致死鎖、更復(fù)雜的錯誤處理及上下文線程的意外阻塞。?此指導(dǎo)原則的例外情況是控制臺應(yīng)用程序的 Main 方法,或是(如果是高級用戶)管理部分異步的基本代碼。
配置上下文
在本文前面,我簡要說明了當(dāng)?shù)却赐瓿?Task 時默認(rèn)情況下如何捕獲“上下文”,以及此捕獲的上下文用于恢復(fù) async 方法的執(zhí)行。?圖 3?中的示例演示在上下文上的恢復(fù)執(zhí)行如何與同步阻塞發(fā)生沖突從而導(dǎo)致死鎖。此上下文行為還可能會導(dǎo)致另一個問題 — 性能問題。?隨著異步 GUI 應(yīng)用程序在不斷增長,可能會發(fā)現(xiàn) async 方法的許多小部件都在使用 GUI 線程作為其上下文。?這可能會形成遲滯,因為會由于“成千上萬的剪紙”而降低響應(yīng)性。
若要緩解此問題,請盡可能等待 ConfigureAwait 的結(jié)果。?下面的代碼段說明了默認(rèn)上下文行為和 ConfigureAwait 的用法:
通過使用 ConfigureAwait,可以實現(xiàn)少量并行性: 某些異步代碼可以與 GUI 線程并行運行,而不是不斷塞入零碎的工作。
除了性能之外,ConfigureAwait 還具有另一個重要方面: 它可以避免死鎖。?再次考慮圖 3;如果向 DelayAsync 中的代碼行添加“ConfigureAwait(false)”,則可避免死鎖。?此時,當(dāng)?shù)却瓿蓵r,它會嘗試在線程池上下文中執(zhí)行 async 方法的剩余部分。?該方法能夠完成,并完成其返回任務(wù),因此不存在死鎖。?如果需要逐漸將應(yīng)用程序從同步轉(zhuǎn)換為異步,則此方法會特別有用。
如果可以在方法中的某處使用 ConfigureAwait,則建議對該方法中此后的每個 await 都使用它。?前面曾提到,如果等待未完成的 Task,則會捕獲上下文;如果 Task 已完成,則不會捕獲上下文。?在不同硬件和網(wǎng)絡(luò)情況下,某些任務(wù)的完成速度可能比預(yù)期速度更快,需要謹(jǐn)慎處理在等待之前完成的返回任務(wù)。?圖 6?顯示了一個修改后的示例。
圖 6 處理在等待之前完成的返回任務(wù)
如果方法中在 await 之后具有需要上下文的代碼,則不應(yīng)使用 ConfigureAwait。?對于 GUI 應(yīng)用程序,包括任何操作 GUI 元素、編寫數(shù)據(jù)綁定屬性或取決于特定于 GUI 的類型(如 Dispatcher/CoreDispatcher)的代碼。?對于 ASP.NET 應(yīng)用程序,這包括任何使用 HttpContext.Current 或構(gòu)建 ASP.NET 響應(yīng)的代碼(包括控制器操作中的返回語句)。?圖 7?演示 GUI 應(yīng)用程序中的一個常見模式:讓 async 事件處理程序在方法開始時禁用其控制,執(zhí)行某些 await,然后在處理程序結(jié)束時重新啟用其控制;因為這一點,事件處理程序不能放棄其上下文。
圖 7 讓 async 事件處理程序禁用并重新啟用其控制
每個 async 方法都具有自己的上下文,因此如果一個 async 方法調(diào)用另一個 async 方法,則其上下文是獨立的。?圖 8?演示的代碼對圖 7?進行了少量改動。
圖 8 每個 async 方法都具有自己的上下文
無上下文的代碼可重用性更高。?嘗試在代碼中隔離上下文相關(guān)代碼與無上下文的代碼,并盡可能減少上下文相關(guān)代碼。?在圖 8?中,建議將事件處理程序的所有核心邏輯都置于一個可測試且無上下文的 async Task 方法中,僅在上下文相關(guān)事件處理程序中保留最少量的代碼。?即使是編寫 ASP.NET 應(yīng)用程序,如果存在一個可能與桌面應(yīng)用程序共享的核心庫,請考慮在庫代碼中使用 ConfigureAwait。
總結(jié)這第三個指導(dǎo)原則便是,應(yīng)盡可能使用 Configure-Await。?無上下文的代碼對于 GUI 應(yīng)用程序具有最佳性能,是一種可在使用部分 async 基本代碼時避免死鎖的方法。?此指導(dǎo)原則的例外情況是需要上下文的方法。
了解您的工具
關(guān)于 async 和 await 有許多需要了解的內(nèi)容,這自然會有點迷失方向。?圖 9?是常見問題的解決方案的快速參考。
圖 9 常見異步問題的解決方案
| 問題 | 解決方案 |
| 創(chuàng)建任務(wù)以執(zhí)行代碼 | Task.Run 或 TaskFactory.StartNew(不是?Task 構(gòu)造函數(shù)或 Task.Start) |
| 為操作或事件創(chuàng)建任務(wù)包裝 | TaskFactory.FromAsync 或 TaskCompletionSource<T> |
| 支持取消 | CancellationTokenSource 和 CancellationToken |
| 報告進度 | IProgress<T> 和 Progress<T> |
| 處理數(shù)據(jù)流 | TPL 數(shù)據(jù)流或被動擴展 |
| 同步對共享資源的訪問 | SemaphoreSlim |
| 異步初始化資源 | AsyncLazy<T> |
| 異步就緒生產(chǎn)者/使用者結(jié)構(gòu) | TPL 數(shù)據(jù)流或 AsyncCollection<T> |
第一個問題是任務(wù)創(chuàng)建。?顯然,async 方法可以創(chuàng)建任務(wù),這是最簡單的選項。?如果需要在線程池上運行代碼,請使用 Task.Run。?如果要為現(xiàn)有異步操作或事件創(chuàng)建任務(wù)包裝,請使用 TaskCompletionSource<T>。下一個常見問題是如何處理取消和進度報告。?基類庫 (BCL) 包括專門用于解決這些問題的類型: CancellationTokenSource/CancellationToken 和 IProgress<T>/Progress<T>。?異步代碼應(yīng)使用基于任務(wù)的異步模式(或稱為 TAP,msdn.microsoft.com/library/hh873175),該模式詳細說明了任務(wù)創(chuàng)建、取消和進度報告。
出現(xiàn)的另一個問題是如何處理異步數(shù)據(jù)流。?任務(wù)很棒,但是只能返回一個對象并且只能完成一次。?對于異步流,可以使用 TPL 數(shù)據(jù)流或被動擴展 (Rx)。?TPL 數(shù)據(jù)流會創(chuàng)建類似于主角的“網(wǎng)格”。?Rx 更加強大和高效,不過也更加難以學(xué)習(xí)。?TPL 數(shù)據(jù)流和 Rx 都具有異步就緒方法,十分適用于異步代碼。
僅僅因為代碼是異步的,并不意味著就安全。?共享資源仍需要受到保護,由于無法在鎖中等待,因此這比較復(fù)雜。?下面是一個異步代碼示例,該代碼如果執(zhí)行兩次,則可能會破壞共享狀態(tài),即使始終在同一個線程上運行也是如此:
int value;
Task<int> GetNextValueAsync(int current);
async Task UpdateValueAsync()
{
? value = await GetNextValueAsync(value);
}
問題在于,方法讀取值并在等待時掛起自己,當(dāng)方法恢復(fù)執(zhí)行時,它假設(shè)值未更改。?為了解決此問題,使用異步就緒 WaitAsync 重載擴展了 SemaphoreSlim 類。?圖 10?演示 SemaphoreSlim.WaitAsync。
圖 10 SemaphoreSlim 允許異步同步
SemaphoreSlim mutex = new SemaphoreSlim(1);
int value;
Task<int> GetNextValueAsync(int current);
async Task UpdateValueAsync()
{
? await mutex.WaitAsync().ConfigureAwait(false);
? try
? {
??? value = await GetNextValueAsync(value);
? }
? finally
? {
??? mutex.Release();
? }
}
異步代碼通常用于初始化隨后會緩存并共享的資源。?沒有用于此用途的內(nèi)置類型,但是 Stephen Toub 開發(fā)了 AsyncLazy<T>,其行為相當(dāng)于 Task<T> 和 Lazy<T> 合二為一。?該原始類型在其博客 (bit.ly/dEN178) 上進行了介紹,并且在我的 AsyncEx 庫 (nitoasyncex.codeplex.com) 中提供了更新版本。
最后,有時需要某些異步就緒數(shù)據(jù)結(jié)構(gòu)。?TPL 數(shù)據(jù)流提供了 BufferBlock<T>,其行為如同異步就緒生產(chǎn)者/使用者隊列。?而 AsyncEx 提供了 AsyncCollection<T>,這是異步版本的 BlockingCollection<T>。
我希望本文中的指導(dǎo)原則和指示能有所幫助。?異步真的是非常棒的語言功能,現(xiàn)在正是開始使用它的好時機!
轉(zhuǎn)載于:https://www.cnblogs.com/Zhaowh/p/3904018.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的异步编程中的最佳做法(Async/Await) --转的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 前端跨域解决方案
- 下一篇: java ee 中文乱码的问题