關於Async與Await的FAQ
=============C#.Net 篇目錄==============html
環境: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 社區測試試用版,就是通常的測試版本)異步
術語:async
APM 異步編程模型,Asynchronous Programming Model異步編程
EAP 基於事件的異步編程模式,Event-based Asynchronous Patternpost
TAP 基於任務的異步編程模式,Task-based Asynchronous Pattern性能
我經常收到來自開發人員的一些問題,這些問題主要集中在C#和Visual Basic中的新關鍵字」async」和」await」。我已經將這些問題分類整理,並藉此機會分享給你們。測試
概念概述優化
- 從哪能得到關於」async」和」await」主題的優秀資源?
一般,你能在Visual Studio Async主題中找到不少資源(eg:文章、視頻、博客等等)。2011年10月份的MSDN雜誌包含了三篇介紹」async」和」await」主題的優秀文章。若是你閱讀,我推薦你閱讀順序依次爲:ui
.NET團隊博客一樣也有」async」和」await」主題的優秀資源:《Async in .NET4.5: 值得期待》
- 爲何須要編譯器幫助咱們完成異步編程?
Anders Hejlsberg’s在2011 微軟Build大會上花了1個小時來幫咱們說明爲何編譯器在這裏真的有用,視頻:《C#和Visual Basic將來的發展方向》。簡而言之,傳統的異步編程模型(APM或EAP)要求你手寫大量代碼(eg:連續傳遞委託、回調)來實現,而且這些代碼會致使語句控制流混亂顛倒。經過.NET4.5提供的新的編程模型(TAP),你能夠像在寫同步代碼同樣使用常規的順序控制流結合並行任務及」async」和」await」關鍵字來完成異步編程,編譯器在後臺應用必要的轉換以使用回調方式來避免阻塞線程。
- 經過Task.Run() 將同步方法包裝成異步任務是否真的有益處?
這取決於你的目標,你爲何要異步調用方法。若是你的目標只是想將當前任務切換到另外一個線程執行,好比,保證UI線程的響應能力,那麼確定有益。若是你的目標是爲了提升可擴展性,那麼使用Task.Run() 包裝成異步調用將沒有任何實際意義。更多信息,請看《我是否應該公開同步方法對應的異步方法API?》。經過Task.Run() 你能夠很輕鬆的實現從UI線程分擔工做到另外一個工做線程,且可協調後臺線程一旦完成工做就返回到UI線程。
「async」關鍵字
- 將關鍵字」async」應用到方法上的做用是什麼?
當你用關鍵字」async」標記一個方法時,即告訴了編譯器兩件事:
1) 你告訴編譯器,想在方法內部使用」await」關鍵字(只有標記了」async」關鍵字的方法或lambda表達式才能使用」await」關鍵字)。這樣作後,編譯器會將方法轉化爲包含狀態機的方法(相似的還有yield的工做原理,請看 《C#穩固基礎:傳統遍歷與迭代器》 ),編譯後的方法能夠在await處掛起而且在await標記的任務完成後異步喚醒。
2) 你告訴編譯器,方法的結果或任何可能發生的異常都將做爲返回類型返回。若是方法返回Task或Task<TResult>,這意味着任何結果值或任何在方法內部未處理的異常都將存儲在返回的Task中。若是方法返回void,這意味着任何異常會被傳播到調用者上下文。
- 被」async」關鍵字標記的方法的調用都會強制轉變爲異步方式嗎?
不會,當你調用一個標記了」async」關鍵字的方法,它會在當前線程以同步的方式開始運行。因此,若是你有一個同步方法,它返回void而且你作的全部改變只是將其標記的」async」,這個方法調用依然是同步的。返回值爲Task或Task<TResult>也同樣。
方法用」async」關鍵字標記不會影響方法是同步仍是異步運行並完成,而是,它使方法可被分割成多個片斷,其中一些片斷可能異步運行,這樣這個方法可能異步完成。這些片斷界限就出如今方法內部顯示使用」await」關鍵字的位置處。因此,若是在標記了」async」的方法中沒有顯示使用」await」,那麼該方法只有一個片斷,而且將以同步方式運行並完成。
- 「async」關鍵字會致使調用方法被排隊到ThreadPool嗎?會建立一個新的線程嗎?
全都不會,」async」關鍵字指示編譯器在方法內部可能會使用」await」關鍵字,這樣該方法就能夠在await處掛起而且在await標記的任務完成後異步喚醒。這也是爲何編譯器在編譯」async」 標記的方法時,方法內部沒有使用」await」會出現警告的緣由。
- 」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表達式不能被轉換爲表達式樹。
- 是否有任何約定,這時應該使用」async」標記方法?
有,基於任務的異步編程模型(TAP)是徹底專一於怎樣實現異步方法,這個方法返回Task或Task<TResult>。這包括(但不限於)使用」async」和」await」關鍵字實現的方法。想要深刻TAP,請看《基於任務的異步編程模型》文檔。
- 「async」標記的方法建立的Tasks是否須要調用」Start()」?
不須要,TAP方法返回的Tasks是已經正在操做的任務。你不只不須要調用」Start()」,並且若是你嘗試也會失敗。更多細節,請看《FAQ on Task.Start》 。
- 「async」標記的方法建立的Tasks是否須要調用」Dispose()」?
不須要,通常來講,你不須要 Disposer() 任何任務。請看《我須要釋聽任務嗎?》。
- 「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」關鍵字
- 「await」關鍵字作了什麼
「await」關鍵字告訴編譯器在」async」標記的方法中插入一個可能的掛起/喚醒點。
邏輯上,這意味着當你寫」await someObject;」時,編譯器將生成代碼來檢查someObject表明的操做是否已經完成。若是已經完成,則從await標記的喚醒點處繼續開始同步執行;若是沒有完成,將爲等待的someObject生成一個continue委託,當someObject表明的操做完成的時候調用continue委託。這個continue委託將控制權從新返回到」async」方法對應的await喚醒點處。
返回到await喚醒點處後,無論等待的someObject是否已經經完成,任何結果均可從Task中提取,或者若是someObject操做失敗,發生的任何異常隨Task一塊兒返回或返回給SynchronizationContext。
在代碼中,意味着當你寫:
Await someObject;
編譯器會將源代碼轉化爲相似以下形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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();
…
}
}
|
- 什麼是」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》。
- 哪些地方不能使用」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) Building Async Coordination Primitives, Part 1: AsyncManualResetEvent
b) Building Async Coordination Primitives, Part 2: AsyncAutoResetEvent
c) Building Async Coordination Primitives, Part 3: AsyncCountdownEvent
d) Building Async Coordination Primitives, Part 4: AsyncBarrier
e) Building Async Coordination Primitives, Part 5: AsyncSemaphore
f) Building Async Coordination Primitives, Part 6: AsyncLock
g) Building Async Coordination Primitives, 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」子句。
- 「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) 不要忘記完成你的任務
- 「task.Result」與」task.GetAwaiter().GetResult()」之間存在功能區別嗎?
存在。但僅僅在任務以非成功狀態完成的狀況下。若是task是以RanToCompletion狀態完成,那麼這兩個語句是等價的。然而,若是task是以Faulted或Canceled狀態完成,task.Result將傳播一個或多個異常封裝而成的AggregateException對象;而」task.GetAwaiter().GetResult()」將直接傳播異常(若是有多個任務,它只會傳播其中一個)。關於爲何會存在這個差別,請看《.NET4.5中任務的異常處理》。
- 「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委託。
- 在控制檯程序中能使用」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
- 「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>
- 編譯器對async/await生成的代碼是否能高效異步執行?
大多數狀況下,是的。由於大量的生成代碼已經被編譯器所優化而且.NET Framework也爲生成代碼創建依賴關係。要了解更多信息,包括使用async/await的最小化開銷的最佳實踐等。請看
2) 2012年MVP峯會上的「The Zen of Async」