# ConfigureAwait常見問題解答

原文: https://devblogs.microsoft.com/dotnet/configureawait-faq/git

.NET 在七多年前在語言和類庫添加了 async/await 。在那個時候,它像野火同樣流行,不只遍佈.NET生態系統,並且還能夠以多種其餘語言和框架進行復制。在利用異步的其餘語言構造,提供異步支持的API以及進行async/ await相關的基礎架構方面的基本改進方面,.NET也實現了不少改進(特別是.NET Core的性能和支持診斷的改進) 。github

可是,async/ await依舊引發疑問的一個方面是ConfigureAwait在這篇文章中,我但願回答其中的許多問題。我但願這篇文章從頭至尾都是可讀的,而且是能夠用做未來參考的常見問題解答(FAQ)列表。api

要真正理解ConfigureAwait,咱們須要提早一點開始…網絡

什麼是SynchronizationContext?

System.Threading.SynchronizationContext 文檔這樣描述SynchronizationContext:它在各類同步模型中提供傳輸同步上下文的基本功能。這並非一個顯而易懂的描述。架構

對於99.9%的狀況,SynchronizationContext僅是一種提供虛擬Post方法的類型,該方法須要委託以異步方式執行(還有各在SynchronizationContext上的各類其餘虛擬成員,但它們的使用量少得多,所以與本討論無關) 。基本類型的Post字面意義只是異步調用 ThreadPool.QueueUserWorkItem以提供的委託。可是,派生類型將覆蓋Post以使該委託可以在最合適的位置和最合適的時間執行。併發

例如,Windows Forms 具備SynchronizationContext派生的類型,該類型將重寫Post以等同於Control.BeginInvoke;這意味着對它的Post方法的任何調用都將致使稍後在與該相關控件關聯的線程(也稱爲「 UI線程」)上調用委託。Windows Forms依賴Win32消息處理,而且在UI線程上運行「消息循環」,該線程只是等待新消息到達以進行處理。這些消息可能用於鼠標移動和單擊,用於鍵盤鍵入,用於系統事件,可供可調用的委託等。所以,給定SynchronizationContextWindows Forms應用程序的UI線程的實例,以使委託在其上執行UI線程,只須要將其傳遞給Post框架

Windows Presentation Foundation(WPF)也是如此。它具備本身的SynchronizationContext派生類型,並帶有Post覆蓋,該覆蓋相似地(經過Dispatcher.BeginInvoke)「封送」 UI線程的委託,在這種狀況下,由WPF Dispatcher而不是Windows Forms Control管理。異步

對於Windows運行時(WinRT)。它具備本身的SynchronizationContext派生類型並帶有Post重寫,該重寫也經過將該隊列排隊到UI線程CoreDispatcherasync

這超出了「在UI線程上運行此委託」的範圍。任何人均可以SynchronizationContext使用Post作任何事情的實現 。例如,我可能不在意委託在哪一個線程上運行,可是我想確保Post對個人全部委託SynchronizationContext都以必定程度的併發度執行。我能夠經過這樣的自定義來實現SynchronizationContextide

internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext
{
    private readonly SemaphoreSlim _semaphore;

    public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel) =>
        _semaphore = new SemaphoreSlim(maxConcurrencyLevel);

    public override void Post(SendOrPostCallback d, object state) =>
        _semaphore.WaitAsync().ContinueWith(delegate
        {
            try { d(state); } finally { _semaphore.Release(); }
        }, default, TaskContinuationOptions.None, TaskScheduler.Default);

    public override void Send(SendOrPostCallback d, object state)
    {
        _semaphore.Wait();
        try { d(state); } finally { _semaphore.Release(); }
    }
}

實際上,單元測試框架xunit 提供了SynchronizationContext與之很是類似的功能,它用於限制與能夠並行運行的測試相關的代碼量。

全部這些的好處與任何抽象都是同樣的:它提供了一個API,可用於將委託排隊,以處理實現的建立者所但願的,而無需瞭解該實現的細節。所以,若是我正在編寫一個庫,而且想開始作一些工做,而後將一個表明排隊回到原始位置的「上下文」,則只須要抓住它們SynchronizationContext,而後堅持下去,而後個人工做已經完成,請Post在該上下文上調用以移交我要調用的委託。我不須要知道對於Windows Forms我應該抓住Control並使用它BeginInvoke,或者對於WPF我應該抓住Dispatcher並使用它BeginInvoke,或者對於xunit我應該以某種方式獲取其上下文並排隊。我只須要抓住當前SynchronizationContext並在之後使用。爲此,SynchronizationContext提供一個Current屬性,以便實現上述目標,我能夠編寫以下代碼:

public void DoWork(Action worker, Action completion)
{
    SynchronizationContext sc = SynchronizationContext.Current;
    ThreadPool.QueueUserWorkItem(_ =>
    {
        try { worker(); }
        finally { sc.Post(_ => completion(), null); }
    });
}

想要從中公開自定義上下文的框架Current使用此SynchronizationContext.SetSynchronizationContext方法。

什麼是TaskScheduler?

SynchronizationContext是「調度程序」的通常抽象。各個框架有時會對調度程序有本身的抽象,System.Threading.Tasks也不例外。當Task由委託支持時,能夠將它們排隊並執行,它們與關聯System.Threading.Tasks.TaskScheduler。就像SynchronizationContext提供了一種虛擬Post方法來排隊委託的調用(經過稍後的實現經過典型的委託調用機制調用委託)同樣,TaskScheduler提供了抽象的QueueTask方法(經過實現的稍後Task經過ExecuteTask方法調用委託)。

返回的默認調度程序TaskScheduler.Default是線程池,可是能夠派生TaskScheduler並覆蓋相關方法,以實如今什麼時候何地調用的Task行爲。例如,核心庫包括System.Threading.Tasks.ConcurrentExclusiveSchedulerPair類型。此類的實例公開兩個TaskScheduler屬性,一個稱爲ExclusiveScheduler,一個稱爲ConcurrentScheduler。安排到的任務ConcurrentScheduler能夠同時運行,可是要受其ConcurrentExclusiveSchedulerPair構建時的限制(相似於前面顯示的MaxConcurrencySynchronizationContext),而且ConcurrentScheduler TaskTask計劃到運行時將不運行ExclusiveScheduler,一次只能Task運行一個互斥對象…這樣,它的行爲很是相似於讀/寫鎖。

相似SynchronizationContextTaskScheduler還具備一個Current屬性,該屬性返回「當前的TaskScheduler。不相似於SynchronizationContext,然而,這沒有設置當前調度程序的方法。相反,當前調度程序是與當前正在運行Task的調度程序相關聯的調度程序,而且調度程序做爲啓動的一部分提供給系統Task。所以,舉例來講,使用這個程序將輸出「True」,使用lambda在StartNew上執行ConcurrentExclusiveSchedulerPairExclusiveScheduler,將看到TaskScheduler.Current設置爲調度程序:

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        var cesp = new ConcurrentExclusiveSchedulerPair();
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler);
        }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler).Wait();
    }
}

有趣的是,TaskScheduler提供了一個靜態FromCurrentSynchronizationContext方法,它建立了一個新的TaskSchedulerSynchronizationContext.Current返回的內容上排隊運行Post

SynchronizationContext和TaskScheduler與等待如何關聯?

考慮在UI應用使用編Button。在點擊Button,咱們要下載從網站一些文字,並將其設置爲ButtonContent。該Button只應該從擁有它的UI線程訪問,因此當咱們已經成功地下載了新的日期和時間文本和想把它存回ButtonContent,咱們須要從擁有控制線程這樣作。若是不這樣作,則會出現相似如下的異常:

System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'

若是咱們手動將其寫出,則可使用SynchronizationContext如前所示的將Content的設置爲原始上下文,例如經過TaskScheduler:

private static readonly HttpClient s_httpClient = new HttpClient();

private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
    s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
    {
        downloadBtn.Content = downloadTask.Result;
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

或者直接使用SynchronizationContext

private static readonly HttpClient s_httpClient = new HttpClient();

private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
    SynchronizationContext sc = SynchronizationContext.Current;
    s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
    {
        sc.Post(delegate
        {
            downloadBtn.Content = downloadTask.Result;
        }, null);
    });
}

不過,這兩種方法都明確使用回調。相反,咱們想天然地用async/await

private static readonly HttpClient s_httpClient = new HttpClient();

private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
    string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
    downloadBtn.Content = text;
}

這個「正確操做」,成功地設置Content在UI線程上,由於就像上面手動實現的版本同樣。await回默認使用SynchronizationContext.Current以及TaskScheduler.Current。當你在C#中await任何東西,編譯器轉換代碼會詢問(經過調用GetAwaiter)的「awaitable」(在這種狀況下,Task)一個「awaiter」(在這種狀況下,TaskAwaiter<string>)。該awaiter負責掛接回調(一般稱爲「繼續」),該回調將在等待的對象完成時回調到狀態機中,並使用在回調時捕獲的任何上下文/調度程序來完成此操做。註冊。雖然不徹底是所使用的代碼(使用了其餘優化和調整),但其實是這樣的:

object scheduler = SynchronizationContext.Current;
if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
{
    scheduler = TaskScheduler.Current;
}

換句話說,它首先檢查是否存在SynchronizationContext集合,若是沒有,則在運行中是否存在非默認值TaskScheduler。若是找到一個,則在準備好調用回調時,它將使用捕獲的調度程序;不然,將使用捕獲的調度程序。不然,它一般只會在完成等待任務的操做中執行回調。

ConfigureAwait(false)有什麼做用?

ConfigureAwait方法並不特殊:編譯器或運行時不會以任何特殊方式對其進行識別。它只是一個返回結構(a ConfiguredTaskAwaitable)的方法,該結構包裝了調用它的原始任務以及指定的布爾值。請記住,它await能夠與任何公開正確模式的類型一塊兒使用。經過返回不一樣的類型,這意味着當編譯器訪問GetAwaiter方法(模式的一部分)時,它是根據從返回的類型ConfigureAwait而不是直接從任務返回的類型來執行此操做的,而且提供了一個掛鉤來更改行爲await經過此自定義等候者的行爲方式。

具體來講,等待ConfigureAwait(continueOnCapturedContext: false)而不是Task直接返回返回的類型最終會影響前面顯示的邏輯,以捕獲目標上下文/計劃程序。它有效地使前面顯示的邏輯更像這樣:

object scheduler = null;
if (continueOnCapturedContext)
{
    scheduler = SynchronizationContext.Current;
    if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
    {
        scheduler = TaskScheduler.Current;
    }
}

換句話說,經過指定false,即便有當前上下文或調度程序要回調,它也會僞裝沒有。

我爲何要使用ConfigureAwait(false)?

ConfigureAwait(continueOnCapturedContext: false)用於避免強制在原始上下文或調度程序上調用回調。這有一些好處:

提升性能。對回調進行排隊而不是僅僅調用它是有代價的,這不只是由於涉及額外的工做(一般是額外的分配),並且還由於它意味着咱們沒法在運行時使用某些優化方法(當咱們確切知道回調將如何調用時,咱們能夠進行更多優化,可是若是將其移交給抽象的任意實現,則有時會受到限制。對於很是熱的路徑,即便檢查當前SynchronizationContext和當前TaskScheduler(這二者都涉及訪問線程靜態數據)的額外成本也可能增長可衡量的開銷。若是後面的代碼await實際上並不須要在原始上下文中運行,請使用ConfigureAwait(false)能夠避免全部這些開銷:它不須要沒必要要的排隊,它能夠利用它能夠召集的全部優化方法,而且能夠避免沒必要要的線程靜態訪問。

避免死鎖。考慮一種用於await某些網絡下載結果的庫方法。調用此方法,並同步地阻塞等待它完成,例如經過使用.Wait().Result.GetAwaiter().GetResult()關閉返回的Task對象。如今考慮會發生什麼,若是你對它的調用發生在當前SynchronizationContext是一個限制,能夠在其上運行1操做的次數,不管是經過什麼樣的明確MaxConcurrencySynchronizationContext這個是一個背景下,只有一個提到的方式,或含蓄可使用的線程,例如UI線程。所以,您能夠在那個線程上調用該方法,而後將其阻塞,以等待操做完成。該操做將啓動網絡下載並等待它。因爲默認狀況下等待Task它將捕獲當前SynchronizationContext,當網絡下載完成時,它將排隊返回SynchronizationContext將調用該操做其他部分的回調。可是,當前惟一能夠處理排隊回調的線程已被您的代碼阻塞所阻塞,等待操做完成。而且該操做要等到回調被處理後才能完成。僵局!即便上下文不將併發限制爲1,而是以任何方式限制資源,這也能夠適用。想象一下相同的狀況,除了使用MaxConcurrencySynchronizationContext限制爲4。咱們不只對該操做進行一次調用,還對上下文進行了4次調用排隊,每一個調用都進行了調用並阻塞了等待它完成的調用。如今,咱們在等待異步方法完成時仍然阻塞了全部資源,惟一容許這些異步方法完成的事情是,是否能夠經過已經被徹底消耗掉的上下文處理它們的回調。再次,僵局!若是庫方法已使用ConfigureAwait(false),則它不會將回調排隊回到原始上下文,避免出現死鎖狀況。

我爲何要使用ConfigureAwait(true)?

不會的,除非您純粹將其用做代表您有意未使用的指示ConfigureAwait(false)(例如,使靜態分析警告等保持沉默)。ConfigureAwait(true)沒有任何意義。await task與進行比較時await task.ConfigureAwait(true),它們在功能上是相同的。若是您ConfigureAwait(true)在生產代碼中看到,則能夠刪除它而不會產生不良影響。

ConfigureAwait方法接受布爾值,由於在某些特殊狀況下,您須要傳遞變量來控制配置。可是99%的用例具備硬編碼的錯誤參數值ConfigureAwait(false)

何時應該使用ConfigureAwait(false)?

這取決於:您是在實現應用程序級代碼仍是通用庫代碼?

編寫應用程序時,一般須要默認行爲(這就是爲何它是默認行爲)。若是應用程序模型/環境(例如Windows窗體,WPF,ASP.NET Core等)發佈了自定義SynchronizationContext,則幾乎能夠確定有一個很好的理由:它爲關心同步上下文與代碼交互的代碼提供了一種方法。應用模型/環境。因此,若是你在Windows編寫的事件處理程序窗體應用程序,書寫xUnit的單元測試,在ASP.NET MVC控制器編寫代碼時,應用模式是否也其實發布SynchronizationContext,您但願使用SynchronizationContext,若是它存在。這意味着默認值/ ConfigureAwait(true)。您簡單地使用await,而且正確的事情發生在將回調/繼續發佈回原始上下文(若是存在)的方面。這致使瞭如下通常指導:若是您正在編寫應用程序級代碼,請不要使用ConfigureAwait(false)。若是您回想一下本文前面的Click事件處理程序代碼示例:

private static readonly HttpClient s_httpClient = new HttpClient();

private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
    string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
    downloadBtn.Content = text;
}

downloadBtn.Content = text須要在原始上下文中完成設置。若是代碼違反了該準則,而是ConfigureAwait(false)在不該當遵循的準則下使用:

private static readonly HttpClient s_httpClient = new HttpClient();

private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
    string text = await s_httpClient.GetStringAsync("http://example.com/currenttime").ConfigureAwait(false); // bug
    downloadBtn.Content = text;
}

會致使不良行爲。依賴於經典ASP.NET應用程序中的代碼也是如此HttpContext.Current;使用ConfigureAwait(false)而後嘗試使用HttpContext.Current可能會致使問題。

相反,通用庫是「通用的」,部分緣由是它們不關心使用它們的環境。您能夠從Web應用程序,客戶端應用程序或測試中使用它們,這可有可無,由於庫代碼對於可能使用的應用程序模型是不可知的。不可知論則也意味着它不會作某種須要以特定方式與應用程序模型交互的事情,例如,它將不會訪問UI控件,由於通用庫對UI控件一無所知。因爲咱們不須要在任何特定環境中運行代碼,所以能夠避免將繼續/回調強制回到原始上下文,而咱們能夠經過使用ConfigureAwait(false)並得到其帶來的性能和可靠性優點來作到這一點。這致使如下方面的通常指導:若是您要編寫通用庫代碼,請使用ConfigureAwait(false)。例如,這就是爲何您會看到await在.NET Core運行時庫中的每一個(或幾乎每一個)都在ConfigureAwait(false)every上使用的緣由await。除少數例外,若是不是這樣,極可能會修復一個錯誤。例如,此PR修復了中的丟失ConfigureAwait(false)呼叫HttpClient

固然,與全部指南同樣,在沒有意義的地方也可能會有例外。例如,通用庫中較大的豁免項(或至少須要考慮的類別)之一是當這些庫具備可調用委託的API時。在這種狀況下,庫的調用者正在傳遞可能由庫調用的應用程序級代碼,而後有效地呈現了庫模擬的那些「通用」假設。例如,考慮LINQ的Where方法的異步版本,例如public static async IAsyncEnumerable<T> WhereAsync(this IAsyncEnumerable<T> source, Func<T, bool> predicate)。是否predicate須要在調用SynchronizationContext方的原始位置上從新調用?這取決於WhereAsync決定的實現,這是它可能選擇不使用的緣由ConfigureAwait(false)

即便有這些特殊狀況,通用指南仍然是一個很好的起點:ConfigureAwait(false)若是要編寫通用庫/與應用程序模型無關的代碼,請使用此指南,不然請不要使用。

ConfigureAwait(false)是否保證回調不會在原始上下文中運行?

不。它保證它不會被排隊回到原始上下文中……但這並不意味着await task.ConfigureAwait(false)以後的代碼仍沒法在原始上下文中運行。那是由於等待已經完成的等待對象只是保持await同步運行,而不是強迫任何東西排隊。所以,若是您await的任務在等待時已經完成,不管您是否使用過ConfigureAwait(false),緊隨其後的代碼將在當前上下文中繼續在當前線程上執行。

在個人方法中僅在第一次等待時使用ConfigureAwait(false)能夠嗎?

通常來講,沒有。請參閱前面的常見問題解答。若是await task.ConfigureAwait(false)涉及到的任務在等待時已經完成(這其實是很常見的),則這ConfigureAwait(false)將毫無心義,由於線程在此以後繼續在該方法中執行代碼,而且仍在與以前相同的上下文中執行。

一個值得注意的例外是,若是您知道第一個await老是將異步完成,而且正在等待的事物將在沒有自定義SynchronizationContext或TaskScheduler的環境中調用其回調。例如,CryptoStream在.NET運行時庫中,要確保其潛在的計算密集型代碼不會做爲調用方的同步調用的一部分運行,所以它使用自定義的等待程序來確保第await一個以後的全部內容都在線程池線程上運行。可是,即便在那種狀況下,您也會注意到next await仍然使用ConfigureAwait(false); 從技術上講這不是必需的,可是它使代碼檢查變得容易得多,由於不然每次查看此代碼時都不須要進行分析以瞭解緣由ConfigureAwait(false) 被遺棄了。

我可使用Task.Run來避免使用ConfigureAwait(false)嗎?

是。若是您寫:

Task.Run(async delegate
{
    await SomethingAsync(); // won't see the original context
});

ConfigureAwait(false)SomethingAsync()調用將是nop,由於傳遞給的委託Task.Run將在線程池線程上執行,而堆棧上沒有更高的用戶代碼,所以SynchronizationContext.Current將返回null。此外,Task.Run隱式使用TaskScheduler.Default,這意味着TaskScheduler.Current在委託內部進行查詢也將返回Default。這意味着await不管是否ConfigureAwait(false)使用,都表現出相同的行爲。它還不能保證此lambda內的代碼能夠作什麼。若是您有代碼:

Task.Run(async delegate
{
    SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx());
    await SomethingAsync(); // will target SomeCoolSyncCtx
});

那麼裏面的代碼SomethingAsync實際上將SynchronizationContext.Current視爲該SomeCoolSyncCtx實例,而且此內部await以及全部未配置的waits SomethingAsync都將發回到該實例。所以,使用這種方法時,您須要瞭解排隊的全部代碼可能作什麼或可能不作什麼,以及它的行爲是否會阻礙您的行爲。

這種方法還以須要建立/排隊其餘任務對象爲代價。這取決於您的性能敏感性,對您的應用程序或庫而言可能可有可無。

還請記住,這些技巧可能會致使更多問題,超出其應有的價值,並帶來其餘意想不到的後果。例如,已經編寫了靜態分析工具(例如Roslyn分析儀)來標記未使用的標誌ConfigureAwait(false),例如CA2007。若是啓用了這樣的分析器,可是爲了不使用ConfigureAwait,使用了這樣的技巧,則分析器頗有可能會對其進行標記,這實際上會爲您帶來更多的工做。所以,也許您可​​能因爲其噪音而禁用了分析器,如今您最終錯過了代碼庫中本應使用的其餘位置ConfigureAwait(false)

我可使用SynchronizationContext.SetSynchronizationContext來避免使用ConfigureAwait(false)嗎?

不,也許。這取決於所涉及的代碼。

一些開發人員編寫以下代碼:

Task t;
SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{
    t = CallCodeThatUsesAwaitAsync(); // awaits in here won't see the original context
}
finally { SynchronizationContext.SetSynchronizationContext(old); }
await t; // will still target the original context

但願它可使內部代碼CallCodeThatUsesAwaitAsync將當前上下文視爲null。並且會的。可是,以上內容不會影響await所見內容TaskScheduler.Current,所以,若是此代碼在某個自定義項上運行TaskScheduler,則await內部CallCodeThatUsesAwaitAsync(且未使用ConfigureAwait(false))的仍將看到並排隊返回該自定義項TaskScheduler

全部相同的警告也適用於與上一個Task.Run相關的FAQ中的問題:這種解決方法存在一些性能方面的問題,嘗試中的代碼也能夠經過設置不一樣的上下文(或使用非默認值調用代碼TaskScheduler)來阻止這些嘗試。

使用這種模式,您還須要注意一些細微的變化:

SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{
    await t;
}
finally { SynchronizationContext.SetSynchronizationContext(old); }

看到問題了嗎?這很難看,但也頗有影響力。沒法保證await將最終在原始線程上調用回調/繼續,這意味着SynchronizationContext在原始線程上可能實際上沒有發生將重置回原始線程的事情,這可能致使該線程上的後續工做項看到錯誤上下文(爲解決此問題,編寫自定義上下文的編寫良好的應用程序模型一般會添加代碼以在調用任何其餘用戶代碼以前手動將其重置)。並且即便它確實在同一線程上運行,也可能要等一下子才能使上下文暫時恢復。並且,若是它在其餘線程上運行,可能最終會在該線程上設置錯誤的上下文。等等。很是不理想。

我正在使用GetAwaiter()。GetResult()。我須要使用ConfigureAwait(false)嗎?

不須要,ConfigureAwait僅會影響回調。具體來講,等待者模式要求等待者公開IsCompleted屬性,GetResult方法和OnCompleted方法(可選地帶有UnsafeOnCompleted方法)。ConfigureAwait只會影響的行爲{Unsafe}OnCompleted,所以,若是您只是直接調用等待者的GetResult()方法,則不管您是在上TaskAwaiter仍是在ConfiguredTaskAwaitable.ConfiguredTaskAwaiter行爲上使行爲差爲零。所以,若是您task.ConfigureAwait(false).GetAwaiter().GetResult()在代碼中看到,則能夠將其替換爲task.GetAwaiter().GetResult()(而且還要考慮是否真的要像這樣進行阻塞)。

我知道我在一個永遠不會具備自定義SynchronizationContext或自定義TaskScheduler的環境中運行。我能夠跳過使用ConfigureAwait(false)嗎?

也許。這取決於您對「從不」這一部分的信心。正如前面的常見問題解答中提到的那樣,僅由於您正在使用的應用程序模型未設置自定義且未在自定義SynchronizationContext上調用代碼TaskScheduler並不意味着其餘用戶或庫代碼未設置自定義。所以,您須要確保不是這種狀況,或者至少要肯定是否存在這種風險。

我據說.NET Core中再也不須要ConfigureAwait(false)。真假?

假。在.NET Core上運行時須要它,其緣由與在.NET Framework上運行時徹底相同。在這方面沒有任何改變。

可是,改變的是某些環境是否發佈本身的環境SynchronizationContext。特別是,雖然.NET Framework上的經典ASP.NET具備本身SynchronizationContext的元素,但ASP.NET Core卻沒有。這意味着默認狀況下,在ASP.NET Core應用程序中運行的代碼將看不到 customSynchronizationContext,從而減小了ConfigureAwait(false)在這種環境中運行的須要。

可是,這並不意味着永遠不會有習俗SynchronizationContextTaskScheduler禮物。若是某些用戶代碼(或您的應用程序正在使用的其餘庫代碼)設置了自定義上下文並調用了您的代碼,或者按Task預約的習慣調用了您的代碼TaskScheduler,那麼即便在ASP.NET Core中,您等待的對象也可能會看到非默認上下文或會致使您要使用的調度程序ConfigureAwait(false)。固然,在這種狀況下,若是您避免同步阻塞(不管如何都應避免在Web應用程序中進行阻塞),而且若是您不介意在這種狀況下出現小的性能開銷,則可能無需使用便可擺脫困境ConfigureAwait(false)

能夠在等待IAsyncEnumerable時使用ConfigureAwait?

是。有關示例,請參見此《 MSDN雜誌》文章

await foreach綁定到一個模式,所以儘管它能夠用於枚舉IAsyncEnumerable<T>,但它也能夠用於枚舉暴露正確的API表面積的東西。.NET運行時庫上包括一個ConfigureAwait擴展方法IAsyncEnumerable<T>方法返回一個自定義類型,該自定義類型包裝IAsyncEnumerable<T>和Boolean並公開正確的模式。當編譯器生成對枚舉數MoveNextAsync和DisposeAsync方法的調用時,這些調用是對返回的已配置枚舉數結構類型的調用,而後依次以所需的配置方式執行等待。

當「等待使用」 IAsyncDisposable時可使用ConfigureAwait嗎?

是的,儘管有輕微的併發症。

就像IAsyncEnumerable<T>前面的常見問題解答中所述,.NET運行時庫在上公開了ConfigureAwait擴展方法IAsyncDisposable,而且await using在實現適當的模式(即公開適當的DisposeAsync方法)時將很高興地使用此擴展方法:

await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false))
{
    ...
}

這裏的問題在於,c如今的類型不是MyAsyncDisposableClass,可是至關於System.Runtime.CompilerServices.ConfiguredAsyncDisposable,這是從ConfigureAwait擴展方法on 返回的類型IAsyncDisposable。

爲了解決這個問題,您須要多寫一行:

var c = new MyAsyncDisposableClass();
await using (c.ConfigureAwait(false))
{
    ...
}

如今,c再次須要類型MyAsyncDisposableClass。這也有增長範圍的做用c; 若是有影響,則能夠將整個內容括在大括號中。

我使用了ConfigureAwait(false),可是個人AsyncLocal等待以後仍然流向代碼。那是個錯誤嗎?

不,這是預期的。AsyncLocal<T>數據流做爲的一部分ExecutionContext,與分開SynchronizationContext。除非您顯式禁用ExecutionContextExecutionContext.SuppressFlow(),不然ExecutionContext(,所以AsyncLocal<T>數據)始終將流經awaits,不管是否ConfigureAwait用於避免捕獲原始SynchronizationContext。有關更多信息,請參閱此博客文章

該語言能夠幫助我避免在個人庫中顯式使用ConfigureAwait(false)嗎?

類庫開發人員有時會對須要使用ConfigureAwait(false)而感到沮喪,並要求侵入性較小的替代方案。

當前沒有任何語言,至少沒有內置在語言/編譯器/運行時中。可是,對於這樣的解決方案可能有不少建議,例如https://github.com/dotnet/csharplang/issues/64五、https://github.com/dotnet/csharplang/issues/254二、https:/ /github.com/dotnet/csharplang/issues/2649和https://github.com/dotnet/csharplang/issues/2746。

若是這對您很重要,或者您以爲這裏有新的有趣的想法,我鼓勵您爲這些或新的討論貢獻本身的想法。

相關文章
相關標籤/搜索