(譯)關於async與await的FAQ

傳送門:異步編程系列目錄……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 Modelide

EAP           基於事件的異步編程模式,Event-based Asynchronous Pattern異步編程

TAP           基於任務的異步編程模式,Task-based Asynchronous Pattern函數

         我經常收到來自開發人員的一些問題,這些問題主要集中在C#Visual Basic中的新關鍵字」async」」await」。我已經將這些問題分類整理,並藉此機會分享給你們。post

 

概念概述性能

1.         從哪能得到關於」async」」await」主題的優秀資源?測試

一般,你能在Visual Studio Async主題中找到不少資源(eg:文章、視頻、博客等等)。201110月份的MSDN雜誌包含了三篇介紹」async」」await」主題的優秀文章。若是你閱讀,我推薦你閱讀順序依次爲:

1) 《經過新的 Visual Studio Async CTP 更輕鬆地進行異步編程》

2) 《經過 Await 暫停和播放》

3) 《瞭解 Async 和 Await 的成本》

.NET團隊博客一樣也有」async」」await」主題的優秀資源:Async in .NET4.5: 值得期待

2.         爲何須要編譯器幫助咱們完成異步編程?

Anders Hejlsberg’s2011 微軟Build大會上花了1個小時來幫咱們說明爲何編譯器在這裏真的有用,視頻:C#Visual Basic將來的發展方向》。簡而言之,傳統的異步編程模型(APMEAP)要求你手寫大量代碼(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)         你告訴編譯器,方法的結果或任何可能發生的異常都將做爲返回類型返回。若是方法返回TaskTask<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」,這個方法調用依然是同步的。返回值爲TaskTask<TResult>也同樣

方法用」async」關鍵字標記不會影響方法是同步仍是異步運行並完成,而是,它使方法可被分割成多個片斷,其中一些片斷可能異步運行,這樣這個方法可能異步完成。這些片斷界限就出如今方法內部顯示使用」await」關鍵字的位置處。因此,若是在標記了」async」的方法中沒有顯示使用」await」,那麼該方法只有一個片斷,而且將以同步方式運行並完成。

3.         「async」關鍵字會致使調用方法被排隊到ThreadPool嗎?會建立一個新的線程嗎?

全都不會,」async」關鍵字指示編譯器在方法內部可能會使用」await」關鍵字,這樣該方法就能夠在await處掛起而且在await標記的任務完成後異步喚醒。這也是爲何編譯器在編譯」async」 標記的方法時,方法內部沒有使用」await」會出現警告的緣由warning CS4014: 因爲不等待此調用,所以會在此調用完成前繼續執行當前方法。請考慮向此調用的結果應用"await"運算符)

4.         」async」關鍵字能標記任何方法嗎?

不能,只有返回類型爲voidTaskTask<TResult>的方法才能」async」標記。而且,並非全部返回類型知足上面條件的方法都能用」async」標記。以下,咱們不容許使用」async」標記方法:

1)         在程序的入口方法(egMain()),不容許。當你正在await的任務還未完成,但執行已經返回給方法的調用者了。EgMain(),這將退出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)         在包含refout參數的方法中,不容許。調用者指望方法同步調用完成時能確保設置參數值,可是標記爲」async」的方法可能不能保證馬上設置參數值直到異步調用完成。

4)         Lambda被用做表達式樹時,不容許。異步lambda表達式不能被轉換爲表達式樹。

5.         是否有任何約定,這時應該使用」async」標記方法?

有,基於任務的異步編程模型(TAP)是徹底專一於怎樣實現異步方法,這個方法返回TaskTask<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」 標記的方法,若是返回TaskTask<TResult>,則沒有方法級的SynchronizationContext交互;對於」async」 標記的方法,若是返回void,則有一個隱藏的SynchronizationContext交互。

當一個」async void」方法被調用,方法調用的開端將捕獲當前SynchronizationContext(「捕獲在這表示訪問它而且將其存儲)。若是這裏有一個非空的SynchronizationContext,將會影響兩件事:(前提:」async void」

1)         在方法調用的開始將致使調用捕獲SynchronizationContext.OperationStarted()方法,而且在完成方法的執行時(不管是同步仍是異步)將致使調用捕獲SynchronizationContext.OprationCompleted()方法。這給上下文引用計數未完成異步操做提供時機點。若是TAP方法返回TaskTask<TResult>,調用者可經過返回的Task作到一樣的跟蹤。

2)         若是這個方法是由於未處理的異常致使方法完成,那麼這個異常將會提交給捕獲的SynchronizationContext。這給上下文一個處理錯誤的時機點。若是TAP方法返回TaskTask<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」

         雖然TaskTask<TResult>是兩個很是廣泛的等待類型(「awaitable」),但這並不表示只有這兩個的等待類型。

「awaitable」能夠是任何類型,它必須公開一個GetAwaiter() 方法而且返回有效的」awaiter」。這個GetAwaiter() 多是一個實例方法(eg:TaskTask<TResult>的實例方法),或者多是一個擴展方法。

「awaiter」」awaitable」對象的GetAwaiter()方法返回的符合特定的模式的類型。」awaiter」必須實現System.Runtime.CompilerServices.INotifyCompletion接口(,並可選的實現System.Runtime.CompilerServices.ICriticalNotifyCompletion接口)。除了提供一個INotifyCompletion接口的OnCompleted方法實現(,可選提供ICriticalNotifyCompletion接口的UnsafeCompleted方法實現),還必須提供一個名爲IsCompletedBoolean屬性以及一個無參的GetResult()方法。GetResult()返回void,若是」awaitable」表明一個void返回操做,或者它返回一個TResult,若是」awaitable」表明一個TResult返回操做。

幾種方法來實現自定義的」awaitable」 談論,請看await anything。也能針對特殊的情景實現自定義」awaitable」,請看Advanced APM Consumption in Async MethodsAwaiting Socket Operations

3.         哪些地方不能使用」await」

1)         在未標記」async」的方法或lambda表達式中,不能使用」await」」async」關鍵字告訴編譯器其標記的方法內部可使用」await」。(更詳細,請看Asynchrony in C# 5 Part Six: Whither async?

2)         在屬性的gettersetter訪問器中,不能使用」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()的調用者,直到這個任務進入最終狀態,這意味着已進入RanToCompletionFaulted,或Canceled完成狀態。相比之下,」await task;」告訴編譯器在」async」標記的方法內部插入一個隱藏的掛起/喚醒點,這樣,若是等待的task沒有完成,異步方法也會立馬返回給調用者,當等待的任務完成時喚醒它從隱藏點處繼續執行。當」await task;」會致使比較多應用程序無響應或死鎖的狀況下使用「task.Wait()」。更多信息請看Await, and UI, and deadlocks! Oh my!

                   當你使用」async」」await」時,還有其餘一些潛在缺陷。Eg

1)         避免傳遞lambda表達式的潛在缺陷

2)         保證」async」方法不要被釋放

3)         不要忘記完成你的任務

4)         使用」await」依然可能存在死鎖

5.         「task.Result」」task.GetAwaiter().GetResult()」之間存在功能區別嗎?

存在。但僅僅在任務以非成功狀態完成的狀況下。若是task是以RanToCompletion狀態完成,那麼這兩個語句是等價的。然而,若是task是以FaultedCanceled狀態完成,task.Result將傳播一個或多個異常封裝而成的AggregateException對象;而」task.GetAwaiter().GetResult()」將直接傳播異常(若是有多個任務,它只會傳播其中一個)。關於爲何會存在這個差別,請看.NET4.5中任務的異常處理》

6.         「await」是如何關聯到當前SynchronizationContext

這徹底取決於被等待的類型。對於給定的」awaitable」,編譯器生成的代碼最終會調用」awaiter」OnCompleted()方法,而且傳遞將執行的continue委託。編譯器生成的代碼對SynchronizationContext一無所知,僅僅依賴當等待的操做完成時調用OnCompleted()方法時所提供的委託。這就是OnCompleted()方法,它負責確保委託在正確的地方被調用,正確的地方徹底由」awaiter」決定。

正在等待的任務(TaskTask<TResult>GetAwaiter方法分別返回的TaskAwaiterTaskAwaiter<TResult>類型)的默認行爲是在掛起前捕獲當前的SynchronizationContext,而後等待task的完成,若是能捕獲到當前的SynchronzationContext,調用continue委託將控制權返回到SynchronizationContext中。因此,例如,若是你在應用程序的UI線程上執行」await task;」,若是當前SynchronizationContext非空則將調用OnCompleted(),而且在任務完成時,將使用UISynchronizationContext傳播continue委託返回到UI線程。

當你等待一個任務,若是沒有當前SynchronizationContext,那麼系統會檢查當前的TaskScheduler,若是有,當task完成時將使用TaskScheduler調度continue委託。

若是SynchronizationContextTaskScheduler都沒有,沒法迫使continue委託返回到原來的上下文,或者你使用」await task.ConfigureAwait(false)代替」await task;」,而後continue委託不會迫使返回到原來上下文而且將容許在系統認爲合適的地方繼續運行。這一般意味着要麼以同步方式運行continue委託不管等待的task在哪完成;要麼使用ThreadPool中的線程運行continue委託。

7.         在控制檯程序中能使用」await」嗎?

固然能。但你不能在Main()方法中使用」await」,由於入口點不能被標記爲」async」。相反,你能在控制檯應用程序的其餘方法中使用」await」。若是你在Main()中調用這些方法,你能夠同步等待(而不是異步等待)他們的完成。Eg

         你還可使用自定義的SynchronizationContextTaskScheduler來實現類似的功能,更多信息請看:

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」,像tasktask<TResult>。示例以下:

1)         Tasks and the APM Pattern

2)         Tasks and the Event-based Asynchronous Pattern

3)         Advanced APM Consumption in Async Methods

4)         Implementing a SynchronizationContext.SendAsync method

5)         Awaiting Socket Operations

6)         await anything

7)         The Nature of TaskCompletionSource<TResult>

9.         編譯器對async/await生成的代碼是否能高效異步執行?

大多數狀況下,是的。由於大量的生成代碼已經被編譯器所優化而且.NET Framework也爲生成代碼創建依賴關係。要了解更多信息,包括使用async/await的最小化開銷的最佳實踐等。請看

1)         .NET4.5TPL的性能提高

2)         2012MVP峯會上的「The Zen of Async」

3)         瞭解 Async 和 Await 的成本

 

 

原文:http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/async-await-faq.aspx

做者:Stephen Toub - MSFT

另外,稍做改動,參考文獻:C# Async Tips and Tricks Part 2 : Async Void

相關文章
相關標籤/搜索