環境:VS2012(盡管System.Threading.Tasks在.net4.0就引入,在.net4.5中為其增加了更豐富的API及性能提升,另外關鍵字”async”和”await”是在C#5.0引入的。vs2010打 Visual Studio Async CTP for VS2010補丁可以引入關鍵字”async”和”await”的支持,但是得不到.net4.5新增API的支持)
(CTP:Community Test Preview 社區測試試用版,就是一般的測試版本)
術語:
APM 異步編程模型,Asynchronous Programming Model
EAP 基於事件的異步編程模式,Event-based Asynchronous Pattern
TAP 基於任務的異步編程模式,Task-based Asynchronous Pattern
我常常收到來自開發人員的一些問題,這些問題主要集中在C#和Visual Basic中的新關鍵字”async”和”await”。我已經將這些問題分類整理,並借此機會分享給大家。
概念概述
1. 從哪能獲得關於”async”和”await”主題的優秀資源?
通常,你能在Visual Studio Async主題中找到很多資源(eg:文章、視頻、博客等等)。2011年10月份的MSDN雜志包含了三篇介紹”async”和”await”主題的優秀文章。如果你閱讀,我推薦你閱讀順序依次為:
.NET團隊博客同樣也有”async”和”await”主題的優秀資源:《Async in .NET4.5: 值得期待》
2. 為什么需要編譯器幫助我們完成異步編程?
Anders Hejlsberg’s在2011 微軟Build大會上花了1個小時來幫我們說明為什么編譯器在這里真的有用,視頻:《C#和Visual Basic未來的發展方向》。簡而言之,傳統的異步編程模型(APM或EAP)要求你手寫大量代碼(eg:連續傳遞委托、回調)來實現,並且這些代碼會導致語句控制流混亂顛倒。通過.NET4.5提供的新的編程模型(TAP),你可以像在寫同步代碼一樣使用常規的順序控制流結合並行任務及”async”和”await”關鍵字來完成異步編程,編譯器在后台應用必要的轉換以使用回調方式來避免阻塞線程。
3. 通過Task.Run() 將同步方法包裝成異步任務是否真的有益處?
這取決於你的目標,你為什么要異步調用方法。如果你的目標只是想將當前任務切換到另一個線程執行,比如,保證UI線程的響應能力,那么肯定有益。如果你的目標是為了提高可擴展性,那么使用Task.Run() 包裝成異步調用將沒有任何實際意義。更多信息,請看《我是否應該公開同步方法對應的異步方法API?》。通過Task.Run() 你可以很輕松的實現從UI線程分擔工作到另一個工作線程,且可協調后台線程一旦完成工作就返回到UI線程。(這里說的可擴展性就如當增加cpu時,Task.Run()並不會增加程序的並行效率,因為他只相當於啟動了一個線程執行任務,倘若使用Parallel.For就具有更好的可擴展性。什么是系統的可擴展性?)
“async”關鍵字
1. 將關鍵字”async”應用到方法上的作用是什么?
當你用關鍵字”async”標記一個方法時,即告訴了編譯器兩件事:
1) 你告訴編譯器,想在方法內部使用”await”關鍵字(只有標記了”async”關鍵字的方法或lambda表達式才能使用”await”關鍵字)。這樣做后,編譯器會將方法轉化為包含狀態機的方法(類似的還有yield的工作原理,請看 《C#穩固基礎:傳統遍歷與迭代器》 ),編譯后的方法可以在await處掛起並且在await標記的任務完成后異步喚醒。
2) 你告訴編譯器,方法的結果或任何可能發生的異常都將作為返回類型返回。如果方法返回Task或Task<TResult>,這意味着任何結果值或任何在方法內部未處理的異常都將存儲在返回的Task中。如果方法返回void,這意味着任何異常會被傳播到調用者上下文。
a) async void函數只能在UI Event回調中使用。
b) async void函數中一定要用try-catch捕獲所有異常,否則會很容易導致程序崩潰。另外需要特別注意lambda表達式,
如:(List<T> 只有 public void ForEach(Action<T> action); 重載)
Enumerable.Range(0, 3).ToList().ForEach(async (i) => { throw new Exception(); });
這段代碼就隱式生成了async void 函數,直接導致了程序的crash。
不過好在,編譯器是優先考慮生成 async Task 形式的匿名函數的。即如下兩個重載,編譯器是使用ForEach(Func<T, Task> action);重載生成async Task 函數。
public void ForEach(Action<T> action); public void ForEach(Func<T, Task> action);
c) 注冊TaskScheduler.UnobservedTaskException事件,記錄Task中未處理異常信息,方便分析及錯誤定位。
2. 被”async”關鍵字標記的方法的調用都會強制轉變為異步方式嗎?
不會,當你調用一個標記了”async”關鍵字的方法,它會在當前線程以同步的方式開始運行。所以,如果你有一個同步方法,它返回void並且你做的所有改變只是將其標記的”async”,這個方法調用依然是同步的。返回值為Task或Task<TResult>也一樣。
方法用”async”關鍵字標記不會影響方法是同步還是異步運行並完成,而是,它使方法可被分割成多個片段,其中一些片段可能異步運行,這樣這個方法可能異步完成。這些片段界限就出現在方法內部顯示使用”await”關鍵字的位置處。所以,如果在標記了”async”的方法中沒有顯示使用”await”,那么該方法只有一個片段,並且將以同步方式運行並完成。
3. “async”關鍵字會導致調用方法被排隊到ThreadPool嗎?會創建一個新的線程嗎?
全都不會,”async”關鍵字指示編譯器在方法內部可能會使用”await”關鍵字,這樣該方法就可以在await處掛起並且在await標記的任務完成后異步喚醒。這也是為什么編譯器在編譯”async” 標記的方法時,方法內部沒有使用”await”會出現警告的原因(warning CS4014: 由於不等待此調用,因此會在此調用完成前繼續執行當前方法。請考慮向此調用的結果應用"await"運算符)。
4. ”async”關鍵字能標記任何方法嗎?
不能,只有返回類型為void、Task或Task<TResult>的方法才能用”async”標記。並且,並不是所有返回類型滿足上面條件的方法都能用”async”標記。如下,我們不允許使用”async”標記方法:
1) 在程序的入口方法(eg:Main()),不允許。當你正在await的任務還未完成,但執行已經返回給方法的調用者了。Eg:Main(),這將退出Main(),直接導致退出程序。
2) 在方法包含如下特性時,不允許。
l [MethodImpl(MethodImplOptions.Synchronized)]
為什么這是不允許的,詳細請看《What’s New for Parallelism in .NET 4.5 Beta》。此特性將方法標記為同步類似於使用lock/SyncLock同步基元包裹整個方法體。
l [SecurityCritical]和[SecuritySafeCritical] (Critical:關鍵)
編譯器在編譯一個”async”標記的方法,原方法體實際上最終被編譯到新生成的MoveNext()方法中,但是其標記的特性依然存在。這意味着特性如[SecurityCritical]不會正常工作。
3) 在包含ref或out參數的方法中,不允許。調用者期望方法同步調用完成時能確保設置參數值,但是標記為”async”的方法可能不能保證立刻設置參數值直到異步調用完成。
4) Lambda被用作表達式樹時,不允許。異步lambda表達式不能被轉換為表達式樹。
5. 是否有任何約定,這時應該使用”async”標記方法?
有,基於任務的異步編程模型(TAP)是完全專注於怎樣實現異步方法,這個方法返回Task或Task<TResult>。這包括(但不限於)使用”async”和”await”關鍵字實現的方法。想要深入TAP,請看《基於任務的異步編程模型》文檔。
6. “async”標記的方法創建的Tasks是否需要調用”Start()”?
不需要,TAP方法返回的Tasks是已經正在操作的任務。你不僅不需要調用”Start()”,而且如果你嘗試也會失敗。更多細節,請看《.NET4.X 並行任務中Task.Start()的FAQ》 。
7. “async”標記的方法創建的Tasks是否需要調用”Dispose()”?
不需要,一般來說,你不需要 Dispose() 任何任務。請看《.NET4.X並行任務Task需要釋放嗎?》。
8. “async”是如何關聯到當前SynchronizationContext?
對於”async” 標記的方法,如果返回Task或Task<TResult>,則沒有方法級的SynchronizationContext交互;對於”async” 標記的方法,如果返回void,則有一個隱藏的SynchronizationContext交互。
當一個”async void”方法被調用,方法調用的開端將捕獲當前SynchronizationContext(“捕獲”在這表示訪問它並且將其存儲)。如果這里有一個非空的SynchronizationContext,將會影響兩件事:(前提:”async void”)
1) 在方法調用的開始將導致調用捕獲SynchronizationContext.OperationStarted()方法,並且在完成方法的執行時(無論是同步還是異步)將導致調用捕獲SynchronizationContext.OprationCompleted()方法。這給上下文引用計數未完成異步操作提供時機點。如果TAP方法返回Task或Task<TResult>,調用者可通過返回的Task做到同樣的跟蹤。
2) 如果這個方法是因為未處理的異常導致方法完成,那么這個異常將會提交給捕獲的SynchronizationContext。這給上下文一個處理錯誤的時機點。如果TAP方法返回Task或Task<TResult>,調用者可通過返回的Task得到異常信息。
當調用”async void”方法時如果沒有SynchronizationContext,沒有上下文被捕獲,然后也不會調用OperaionStarted/OperationCompleted方法。在這種情況下,如果存在一個未處理過的異常在ThreadPool上傳播,那么這會采取線程池線程默認行為,即導致進程被終止。
“await”關鍵字
1. “await”關鍵字做了什么
“await”關鍵字告訴編譯器在”async”標記的方法中插入一個可能的掛起/喚醒點。
邏輯上,這意味着當你寫”await someObject;”時,編譯器將生成代碼來檢查someObject代表的操作是否已經完成。如果已經完成,則從await標記的喚醒點處繼續開始同步執行;如果沒有完成,將為等待的someObject生成一個continue委托,當someObject代表的操作完成的時候調用continue委托。這個continue委托將控制權重新返回到”async”方法對應的await喚醒點處。
返回到await喚醒點處后,不管等待的someObject是否已經經完成,任何結果都可從Task中提取,或者如果someObject操作失敗,發生的任何異常隨Task一起返回或返回給SynchronizationContext。
在代碼中,意味着當你寫:
await someObject;
編譯器會生成一個包含 MoveNext 方法的狀態機類:
private class FooAsyncStateMachine : IAsyncStateMachine { // Member fields for preserving “locals” and other necessary state int $state; TaskAwaiter $awaiter; … public void MoveNext() { // Jump table to get back to the right statement upon resumption switch (this.$state) { … case 2: goto Label2; … } … // Expansion of “await someObject;” this.$awaiter = someObject.GetAwaiter(); if (!this.$awaiter.IsCompleted) { this.$state = 2; this.$awaiter.OnCompleted(MoveNext); return; Label2: } this.$awaiter.GetResult(); … } }
在實例 someObject上使用這些成員來檢查該對象是否已完成(通過 IsCompleted),如果未完成,則掛接一個續體(通過 OnCompleted),當所等待實例最終完成時,系統將再次調用 MoveNext 方法,完成后,來自該操作的任何異常將得到傳播或作為結果返回(通過 GetResult),並跳轉至上次執行中斷的位置。
2. 什么是”awaitables”?什么是”awaiters”?
雖然Task和Task<TResult>是兩個非常普遍的等待類型(“awaitable”),但這並不表示只有這兩個的等待類型。
“awaitable”可以是任何類型,它必須公開一個GetAwaiter() 方法並且返回有效的”awaiter”。這個GetAwaiter() 可能是一個實例方法(eg:Task或Task<TResult>的實例方法),或者可能是一個擴展方法。
“awaiter”是”awaitable”對象的GetAwaiter()方法返回的符合特定的模式的類型。”awaiter”必須實現System.Runtime.CompilerServices.INotifyCompletion接口(,並可選的實現System.Runtime.CompilerServices.ICriticalNotifyCompletion接口)。除了提供一個INotifyCompletion接口的OnCompleted方法實現(,可選提供ICriticalNotifyCompletion接口的UnsafeCompleted方法實現),還必須提供一個名為IsCompleted的Boolean屬性以及一個無參的GetResult()方法。GetResult()返回void,如果”awaitable”代表一個void返回操作,或者它返回一個TResult,如果”awaitable”代表一個TResult返回操作。
幾種方法來實現自定義的”awaitable” 談論,請看《await anything》。也能針對特殊的情景實現自定義”awaitable”,請看《Advanced APM Consumption in Async Methods》和《Awaiting Socket Operations》。
3. 哪些地方不能使用”await”?
1) 在未標記”async”的方法或lambda表達式中,不能使用”await”。”async”關鍵字告訴編譯器其標記的方法內部可以使用”await”。(更詳細,請看《Asynchrony in C# 5 Part Six: Whither async?》)
2) 在屬性的getter或setter訪問器中,不能使用”await”。屬性的意義是快速的返回給調用者,因此不期望使用異步,異步是專門為潛在的長時間運作的操作。如果你必須在你的屬性中使用異步,你可以通過實現異步方法然后在你的屬性中調用。
3) 在lock/SyncLock塊中,不能使用”await”。關於談論為什么不允許,以及SemaphoreSlim.WaitAsync(哪一個能用於此情況的等待),請看《What’s New for Parallelism in .NET 4.5 Beta》。你還可以閱讀如下文章,關於如何構建各種自定義異步同步基元:
a) 構建Async同步基元,Part 1 AsyncManualResetEvent
b) 構建Async同步基元,Part 2 AsyncAutoResetEvent
c) 構建Async同步基元,Part 3 AsyncCountdownEvent
d) 構建Async同步基元,Part 4 AsyncBarrier
e) 構建Async同步基元,Part 5 AsyncSemaphore
f) 構建Async同步基元,Part 6 AsyncLock
g) 構建Async同步基元,Part 7 AsyncReaderWriterLock
4) 在unsafe區域中,不能使用”await”。注意,你能在標記”async”的方法內部使用”unsafe”關鍵字,但是你不能在unsafe區域中使用”await”。
5) 在catch塊和finally塊中,不能使用”await”。你能在try塊中使用”await”,不管它是否有相關的catch塊和finally塊,但是你不能在catch塊或finally塊中使用”await”。這樣做會破壞CLR的異常處理。
6) LINQ中大部分查詢語法中,不能使用”await”。”await”可能只用於查詢表達式中的第一個集合表達式的”from”子句或在集合表達式中的”join”子句。
4. “await task;”和”task.Wait”效果一樣嗎?
不。
“task.Wait()”是一個同步,可能阻塞的調用。它不會立刻返回到Wait()的調用者,直到這個任務進入最終狀態,這意味着已進入RanToCompletion,Faulted,或Canceled完成狀態。相比之下,”await task;”告訴編譯器在”async”標記的方法內部插入一個隱藏的掛起/喚醒點,這樣,如果等待的task沒有完成,異步方法也會立馬返回給調用者,當等待的任務完成時喚醒它從隱藏點處繼續執行。當”await task;”會導致比較多應用程序無響應或死鎖的情況下使用“task.Wait()”。更多信息請看《Await, and UI, and deadlocks! Oh my!》。
當你使用”async”和”await”時,還有其他一些潛在缺陷。Eg:
3) 不要忘記完成你的任務
5. “task.Result”與”task.GetAwaiter().GetResult()”之間存在功能區別嗎?
存在。但僅僅在任務以非成功狀態完成的情況下。如果task是以RanToCompletion狀態完成,那么這兩個語句是等價的。然而,如果task是以Faulted或Canceled狀態完成,task.Result將傳播一個或多個異常封裝而成的AggregateException對象;而”task.GetAwaiter().GetResult()”將直接傳播異常(如果有多個任務,它只會傳播其中一個)。關於為什么會存在這個差異,請看《.NET4.5中任務的異常處理》。
6. “await”是如何關聯到當前SynchronizationContext?
這完全取決於被等待的類型。對於給定的”awaitable”,編譯器生成的代碼最終會調用”awaiter”的OnCompleted()方法,並且傳遞將執行的continue委托。編譯器生成的代碼對SynchronizationContext一無所知,僅僅依賴當等待的操作完成時調用OnCompleted()方法時所提供的委托。這就是OnCompleted()方法,它負責確保委托在”正確的地方”被調用,”正確的地方”完全由”awaiter”決定。
正在等待的任務(由Task和Task<TResult>的GetAwaiter方法分別返回的TaskAwaiter和TaskAwaiter<TResult>類型)的默認行為是在掛起前捕獲當前的SynchronizationContext,然后等待task的完成,如果能捕獲到當前的SynchronzationContext,調用continue委托將控制權返回到SynchronizationContext中。所以,例如,如果你在應用程序的UI線程上執行”await task;”,如果當前SynchronizationContext非空則將調用OnCompleted(),並且在任務完成時,將使用UI的SynchronizationContext傳播continue委托返回到UI線程。
當你等待一個任務,如果沒有當前SynchronizationContext,那么系統會檢查當前的TaskScheduler,如果有,當task完成時將使用TaskScheduler調度continue委托。
如果SynchronizationContext和TaskScheduler都沒有,無法迫使continue委托返回到原來的上下文,或者你使用”await task.ConfigureAwait(false)代替”await task;”,然后continue委托不會迫使返回到原來上下文並且將允許在系統認為合適的地方繼續運行。這通常意味着要么以同步方式運行continue委托,無論等待的task在哪完成;要么使用ThreadPool中的線程運行continue委托。
7. 在控制台程序中能使用”await”嗎?
當然能。但你不能在Main()方法中使用”await”,因為入口點不能被標記為”async”。相反,你能在控制台應用程序的其他方法中使用”await”。如果你在Main()中調用這些方法,你可以同步等待(而不是異步等待)他們的完成。Eg:
你還可以使用自定義的SynchronizationContext或TaskScheduler來實現相似的功能,更多信息請看:
1) Await, SynchronizationContext, and Console Apps: Part 1
2) Await, SynchronizationContext, and Console Apps: Part 2
3) Await, SynchronizationContext, and Console Apps: Part 3
8. “await”能和異步編程模型模式(APM)或基於事件的異步編程模式(EAP)一起使用嗎?
當然能,你可以為你的異步操作實現一個自定義的”awaitable”,或者將你現有的異步操作轉化為現有的”awaitable”,像task或task<TResult>。示例如下:
2) Tasks and the Event-based Asynchronous Pattern
3) Advanced APM Consumption in Async Methods
4) Implementing a SynchronizationContext.SendAsync method
7) The Nature of TaskCompletionSource<TResult>
9. 編譯器對async/await生成的代碼是否能高效異步執行?
大多數情況下,是的。因為大量的生成代碼已經被編譯器所優化並且.NET Framework也為生成代碼建立依賴關系。要了解更多信息,包括使用async/await的最小化開銷的最佳實踐等。請看
2) 2012年MVP峰會上的“The Zen of Async”
原文:http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/async-await-faq.aspx
另外,稍作改動,參考文獻:C# Async Tips and Tricks Part 2 : Async Void