C# 中 ConfigureAwait 相關答疑FAQ

来源:https://www.cnblogs.com/ms27946/archive/2020/01/18/ConfigureAwait-FAQs-In-CSharp.html
-Advertisement-
Play Games

C 中 ConfigureAwait 相關答疑FAQ 在前段時間經常看到園子里有一些文章討論到 ConfigureAwait,剛好今天在微軟官方博客看到了 "Stephen Toub" 前不久的一篇答疑 ConfigureAwait 的一篇文章,想翻譯過來。 原文地址:https://devblog ...


C# 中 ConfigureAwait 相關答疑FAQ

在前段時間經常看到園子里有一些文章討論到 ConfigureAwait,剛好今天在微軟官方博客看到了 Stephen Toub 前不久的一篇答疑 ConfigureAwait 的一篇文章,想翻譯過來。

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

.NET 加入 async/await 特性已經有 7 年了。這段時間,它蔓延的非常快,廣泛;不只在 .NET 生態系統,也出現在其他語言和框架中。在 .NET 中,他見證了許多了改進,利用非同步在其他語言結構(additional language constructs)方面,提供了支持非同步的 API,在基礎設施中標記 async/await 作為最基本的優化(特別是在 .NET Core 的性能和分析能力上)。

然而,async/await 另一方面也帶來了一個問題,那就是 ConfigureAwait。在這片文章中,我會解答它們。我嘗試在這篇文章從頭到尾變得更好讀,能作為一個友好的答疑清單,能為以後提供參考。

什麼是 SynchronizationContext

System.Threading.SynchronizationContext文檔表明它“它提供一個最基本的功能,在各種同步模型中傳遞同步上下文”,除此之外並無其他描述。

對於它的 99% 的使用案例,SynchronizationContext只是一個類,它提供一個虛擬的 Post的方法,它傳遞一個委托在非同步中執行(這裡面其實還有其他很多虛擬成員變數,但是很少用到,並且與我們這次討論毫不相干)。這個類的 Post僅僅只是調用ThreadPool.QueueUserWorkItem來非同步執行前面傳遞的委托。但是,那些繼承類能夠覆寫Post方法,以至於在大多數合適的地方和時間執行。

舉個例子,Windows Forms 有一個SynchronizationContext派生類,它覆寫了Post方法,就等價於Control.BeginInvoke。那就是說所有調用這個Post方法都將會引起這個委托在這個相關控制項關聯的線程上被調用,它被稱為“UI線程”。Windows Forms 依靠 Win32 上的消息處理程式以及有一個“消息迴圈”運行在UI線程上,它簡單的等待新的消息到達來處理。那些消息可能是滑鼠移動和點擊,對於鍵盤輸入、系統事件,委托等都能夠被執行。所以為 Windows Forms 應用程式的 UI 線程提供一個SynchronizationContext實例,讓它能夠得到委托在 UI 線程上執行,需要做的只是簡單的傳遞它給Post

對於 WPF 來說也是如此。它也有它自己的SynchronizationContext派生類,覆寫了Post,類似的,傳遞一個委托給 UI 線程(與之對應 Dispatcher.BeinInvoke),在這個例子中是 WPF Dispatcher 而不是 Windows Forms 控制項。

對於 Windows 運行時(WinRT)。它同樣有自己的SynchronizationContext派生類,覆寫Post,通過CoreDispatcher也傳遞委托給 UI 線程。

這不僅僅只是“在 UI 線程上運行委托”。任何人都能實現SynchronizationContext來覆寫Post來做任何事。例如,我不會關心線程運行委托所做的事,但是我想確保任何在 Post 執行的我編寫的SynchronizationContext都以一定程度的併發度執行。我可以實現它,用我自定義SynchronizationContext類,像下麵一樣:

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,它能夠用來排隊傳遞委托來處理創造者想要實現他們想要的( it provides a single API that can be used to queue a delegate for handling however the creator of the implementation desires),而不需要知道實現的細節。

所有,如果我們在編寫類庫,並且想要進行和執行相同的工作,那麼就排隊委托傳遞迴在原來位置的“上下文”,那麼我就只需要獲取它們的SynchronizationContext,占有它,然後當完成我的工作時調用那個上下文中的Post來調用傳遞我想要調用的委托。於 Windows Forms,我不必知道我應該獲取一個Control並且調用它的BegeinInvoke,或者對於 WPF,我不用知道我應該獲取一個 Dispatcher 並且調用它的 BeginInvoke,又或是在 xunit,我應該獲取它的上下文併排隊傳遞;我只需要獲取當前的SynchronizationContext並調用它。為了這個目的,SynchronizationContext提供一個Currenct屬性,為了實現上面說的,我可以像下麵這樣編寫代碼:

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.Task也不例外(no exception)。當那些入隊列以及執行的那些任務被委托支持(backed)時,它們與System.Threading.Task.TaskScheduler相關。就好比SynchronizationContext提供一個虛擬的Post方法對委托的調用進行排隊(稍後使用實現來通過典型的委托機制來調用委托),TaskScheduler提供一個抽象方法QueueTask(稍微使用實現通過ExecuteTask方法來調用任務)。

預設的調度器會通過TaskScheduler.Default返回的是一個線程池,但是它儘可能的派生自TaskScheduler並腹寫相關的方法,來完成以何時何地的調用任務的行為。舉個例子,核心庫包含System.Threading.Tasks.ConcurrentExclusiveSchedulerPair類型。這個類的實例暴露了兩個 TaskScheduler屬性,一個調用自ExclusiveScheduler,另一個調用自ConcurrentScheduler。那些被調度到ConcurrentScheduler的任務可能是並行運行的,但是在構建它時,會受制於被受限的ConcurrentExclusiveSchedulerPair(與前面展示的MaxConcurrencySynchronizationContext相似),並且當一個任務被調度到ExclusiveScheduler正在運行的時候,ConcurrentScheduler`任務將不會執行,一次只運行一個獨立任務... 這樣的話,它行為就很像一個讀寫鎖。

SynchronizationContextTaskScheduler也有一個Current屬性,它會返回一個“current”Taskscheduler。而不像SynchronizationContext,但是,這裡不存在方法設置當前調度器。相反,當前的調度器是一個與當前正在運行的任務相關,並且作為系統的一部分開始一個任務來提供一個調度器。例如下麵這個程式將會輸出“True”,與StartNew一起使用的lambda在`ConcurrentExclusiveSchedulerPairExclusiveScheduler上,並且將會看到TaskScheduler.Current被設置為調度器(原文:as the lambda used with StartNew is executed on the ConcurrentExclusiveSchedulerPair‘s ExclusiveScheduler and will see TaskScheduler.Current set to that scheduler):

using System;
using System.Threading.Tasks;

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

有趣的是,TaskScheduler提供一個靜態的方法FromCurrentSynchronizationContext,它創建一個新的調度器,那些排隊的任務在任意的返回的SynchronizationContext.Current都會運行,使用它的Post方法為任務進行排隊。

SynchronizationContext和TaskScheduler相關如何等待

考慮到一個 UI app 使用 Button。一旦點擊這個按鈕,我們想要從網站下載一個文本,以及設置這個 Button 的文本內容。並且這個 Button 只能被當前的 UI 線程訪問,該線程擁有它,所以當我們成功下載新的日期和時間文本,並且想要存儲回 Button 的 Content 值,我們只需要做的就是訪問該控制項所屬的線程。如果不這樣,我們就會得到這樣一個錯誤:

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

如果我們手寫出來,我們可以使用前面顯示的SynchronizationContext設置的Current封送回原始上下文,就如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;
}

這麼做才能成功的在 UI 線程上設置 Content 的值,因為這和上面手動實現的版本一樣,在預設情況下,這個正在等待 Task 只會關註SynchronizationContext.Current,與TaskScheduler.Current一樣。在C#中,當你一旦使用 await,編譯器就會轉換代碼去請求(調用GetAwaiter)這個可等待的(在這個例子中就是 Task)等待者(在例子中說的就是TaskAwaiter<string>)(原文:ask the "awaitable" for an "awaiter")。而等待著的責任就是負責連接(調用)回調函數(經常性的作為一個“continuation“),當這個等待的對象已經完成的時候,它會在狀態機里觸發回調,以及只要在回調函數一旦在某個時間點註冊,它所做的就是捕捉上下文/調度器。儘管沒有用確切的代碼(這裡有額外的優化和工作上的調整),它看起來就像這樣:

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

換句話說,就是首先判斷 scheduler 是否有被賦值過,如果沒有,那是否還有非預設的 TaskScheduler。如果有,那麼在當準備好調用回調函數的時候,它將使用的是這個捕捉到的調度器;否則它一般調用回調函數作為這個等待的 task 操作完成時的一部分。

ConfigureAwait(false)做了什麼事

ConfigureAwait方法並沒有什麼特別的:編譯器或者運行時不會以任何特殊的方式識別出它。它只是簡單的返回一個結構體(ConfigureTaskAwaitable),它包裝了原始的task,被調用時指定了一個布爾值。要記住,await能用在任何正確的模式下的任何類。通過返回不同的類型,即當編譯器訪問 GetAwaiter 方法(是這模式的一部分)返回的實例,它是從ConfigureAwait返回的類型,而不是任務task直接返回的,並且它提供了一個鉤子(hook),這個鉤子通過自定義的awaiter改變了行為。

特別是,不是等待從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)主要用來避免在原始上下文或調度器上強制調用回調。這有以下好處:

提高性能。這裡主要的開銷就是回調會排隊入隊列而不僅僅只是調用回調,它們都還要涉及其它額外的工作(比如指定額外的分配),也是因為它在某些我們想要的優化上,在運行時是不能使用的(當我們明確的知道回調函數是如何調用的時候,我們能做更多的優化,但是如果它被隨意的傳遞給一個實現抽象的類,我們有時就會受到限制)。對於每次熱路徑(hot paths),甚至是檢查當前的SynchronizationContext以及TaskScheduler的所花的額外開銷(它們都涉及到訪問靜態線程),這些都會增加一定量的開銷。如果await後邊的代碼實際上在原始上下文中沒有長時間運行,使用ConfigureAwait(false)就能避免前面提到的所有的開銷:它根本不需要入隊列,它能運用它所有能優化的點,並且避免不必要的靜態線程訪問。

避免死鎖。有一個庫方法,它在網路下載資源,併在其結果上使用await。你調用它並且同步阻塞等待結果的返回,比如通過操作返回的Task使用.Wait().Result.GetAwaiter().GetResult()。那現在我們來考慮一下,在當前上下文在受操作數量限制運行為1時(SynchronizationContext),如果你調用它會發生什麼,它是否像早前顯示的MaxConcurrencySynchronizationContext那樣,又或者是隱含的只有一個線程能使用的上下文,例如 UI 線程。所以你在一個線程上調用方法,然後阻塞它到網路下載任務完成。這個操作會啟動網路下載並等待它。因為在預設情況下,這個操作會捕捉當前的同步上下文,之所以它會這麼做,是因為當網路下載任務完成之後,它會入隊列返回SynchronizationContext,回調函數會調用剩餘的操作。(原文: it does so, and when the network download completes, it queues back to the SynchronizationContext the callback that will invoke the remainder of the operation)。但是只有一個線程能處理這個已經入隊列的回調函數,而且就是當前由於你的代碼因這個操作等待完成而被阻塞的線程。這個操作除非這個回調函數已被處理,否則是不會完成的。這就發生了死鎖!(回調函數相關的線程上下文又被阻塞)這種情況也會發生在沒有限制併發,哪怕是1的情況,一旦資源以任何方式受到限制的時候也是如此。除了使用MaxConcurrencySynchronizationContext設置限度為4,想象一下相同的場景。與其只讓其中一個操作調用,我們可以入四個上下文來調用,它們每一個都會調用並阻塞等待它完成。現在我還是阻塞全部的資源,當等待非同步訪問完成的時候,只有一件事,即如果它們的回調函數能夠被完全使用的上下文處理,那麼就允許那些非同步方法完成。再一次,死鎖。

取而代之的是庫方法使用ConfigureAwait(false)`,那它就不會將回調入隊列給原始上下文,這樣就避免了死鎖的場景。

為什麼我會要用到ConfigureAwait(true)

你沒必要要用到,除非你要用它,純粹是想要表明你明確不會使用ConfigureAwait(false)(例如來消除(silence)靜態分析警告或類似的警告)。ConfigureAwait(true)沒有意義。當去比較await taskawait task.ConfigureAwait(true)時,它們是一樣的。如果你在生產代碼中看到有ConfigureAwait(true),你可以毫不猶豫的刪掉它。

ConfigureAwait接受一個布爾值,是因為有一些合適的場景,其中你可能想要一個變數來控制配置。但是99%的使用案例都是使用硬編碼傳遞一個固定的false參數,即ConfigureAwait(false)

合適應該用ConfigureAwait(false)

這取決於:你實現的應用程式代碼或是通用目的的庫代碼?

當在編寫應用程式時,你一般想要預設行為(它為什麼要預設行為)。如果一個app 模型/環境(如Windows Forms,WPF,ASP.NET Core等等)發佈一個自定義的SynchronizationContext,這大部分無疑都有一個好理由:它提供了代碼方式,它關心同步上下文與app模型/環境適當的交互。所以如果你在Windows Forms應用程式編寫一個事件處理程式,在xunit編寫一個單元測試,在ASP.NET MVC編寫一個控制器,無論這個app模型實際上是否發佈了這個SynchronizationContext,如果它存在你就可以想使用它。其意思就是預設情況(即ConfigureAwait(true))。你只需要簡單的使用await,然後正確的事情就會發生,它維護回調/延續會被傳遞迴原始的上下文,如果它存在。這就回產生一個標準:如果你在應用程式級別的代碼,不需要用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");
    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 app使用它們,也可以在客戶端app使用它們,或者是測試,它都不關心,一個類庫被用到哪個app模型是未知的。變得不可未知就是說它們沒準備做任何事,在app中以特殊的方式與之交互,例如它不會訪問 UI 控制項,因為通用類庫對你的 UI 控制項一無所知。由於我們不會在特定的環境中運行代碼,這樣我們就能避免強制continuation/callback回傳給原始上下文,我們做的就是調用ConfigureAwait(false),並且它會帶來性能和可靠性的好處。這樣就會產生通用的準則:如果你在編寫通用類庫,那麼你就應該使用ConfigureAwait(false)。這就是原因,例如,在.NET Core運行時類庫中,你到處可見(或絕大多數)在使用ConfigureAwait(false)的地方使用了await;有極少數例外,如果沒有的話,那有可能是bug被修複了。例如這個PR,它修複了在HttpClient中忘記調用ConfigureAwait(false)

既然是作為準則,當然也有例外的地方它是沒有意義的。舉個例子,有一個較大的例外(或者說至少需要考慮的一種情況),在通用類庫中,那些需要調用的委托的api。這種情況,類庫調用者要傳遞可能會被庫調用的應用程式級別的代碼,這會有效的會使庫的那些通用的假設變得毫無意義(In such cases, the caller of the library is passing potentially app-level code to be invoked by the library, which then effectively renders those “general purpose” assumptions of the library moot)。考慮以下例子,一個非同步版本的 Linq 的 Where 方法如public static async IAsyncEnumerable<T> WhereAsync(this IAsyncEnumerable<T> source, Func<T,bool> predicate)這裡的 predicate 必須要在調用者的原ConfigureAwait(false)

這些特殊的例子,通用的標準就是一個非常好的開始點:如果你正在寫類庫/應用程式級未知的代碼,那麼請使用ConfigureAwait(false),否則不要使用。

ConfigureAwait(false)會保證回調不會在原始上下文運行嗎

不,它保證它不會把回調入隊列到原始上下文。但是這並不意味著在代碼await task.ConfiureAwait(false)後面就不會運行在原始上下文中。那是因為在已經完成的可等待者上等待,它只需要同步的運行await,而不用強制到入隊列返回。所以你在 await 一個 task,它早就在它等待的時間內完成了,無論你是否使用了ConfigureAwait(false),代碼會在之後在當前線程上立即執行,無論這個上下文是否還是當前的。

只在方法中只第一次用await用ConfigureAwait(false)以及剩下的代碼不用可以嗎

一般來說,不行。見上一個FAQ。如果這個await task.ConfigureAwait(false)涉及到這個 task 在其等待的時間內已經完成了(這種情況極其容易發生),那麼ConfigureAwait(false)就顯得沒有意義了,這個線程會繼續執行這個非同步方法之後的代碼,並且與之前具有相同的上下文。

一個重要的例外就是,如果你知道第一次 await 總是會非同步的完成,並且這個等待的將會調用回調,在一個自定義同步上下問和調度器的自由的環境。舉個例子,CryptoStream是.NET運行時類庫的類,它確保了密集型計算的代碼不會作為同步調用者調用的一部分運行,所以它使用了自定義的awaiter來確保所有事情在第一次await之後都會運行線上程池線程下。然而,在那個例子中,你將會註意到下個 await 仍然使用了ConfiureAwait(false);在技術上,這是沒必要的,但是它會讓代碼看起來更加容易,否則每次看到這個代碼的時候,都不要分析去理解為什麼不用ConfiureAwait(false)

我能使用Task.Run從而避免使用ConfigureAwait(false)嗎

對,如果你這麼寫:

Task.Run(async delegate
{
    await SomethingAsync(); // 將看不到原始上下文
});

然後在SomethingAsync()之後調用ConfigureAwait(false)將會是一個空操作,因為這個委托作為參數傳遞給Task.Run,它將線上程池線程上執行,堆棧上沒有更高級別的用戶代碼,如SynchronizationContext.Current就會返回null。儘管如此,Task.Run 隱含的使用了 TaskScheduler.Default,它的意思在裡邊查找 TaskScheduler.Current,其委托也會返回 Default。這意思就是說不管你是否使用了ConfigureAwait(false),它都會展示相同的行為。同時它也不會做任何保證 lambda 裡面的代碼會執行。如果你有如下代碼:

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

然後在 SomethingAsync 裡面的代碼實際上將會看到 SynchronizationContext.Current 實例對象就是 SomeCoolSyncCtx,await 和任何沒有配置的 await,這兩者在 SomethingAsync 內都會返回給它。所以為了使用這個方法,你必須要理解你可能正在排隊的代碼做的所有事情或有可能什麼也沒做,以及這個操作是否會組織你的操作。

這個方法的代價就是需要創建/排隊一個額外的任務對象。這對於你的app或類庫是否重要,取決於你的性能敏感度。

還要記住,這些技巧可能會導致更多問題乃至超過它們的價值,並會產生其他意想不到的結果。例如,靜態分析工具(如 Roslyn 分析器)已經寫了一個去表示等待時它不會使用ConfigureAwait(false),如CA2007。如果你啟用了這樣一個分析器,隨後又使用了一些技巧來避免使用ConfigureAwait(false),那麼分析器就會去標記它,並且實際上會為你做更多事。那麼如果你之後因為它吵鬧(noisiness)又關閉了分析器,最後你會在代碼里會丟失你實際上應該要調用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:這裡的變通方法有性能的含義,而在 try 中的代碼也可以通過設置不同的上下文來組織這些嘗試(或者通過非預設的調度器調用代碼)。

使用這種模式,你需要小心這種細微的差異:

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

發現問題了麽?這是很難發現但是影響很大的。這裡它無法保證 await 將會在原始上下文中調用 callback/continuation,這個意思就是說重新設置 SynchronizationContext 返回給原始上下文也許不會發生在原始線程,這會導致在這個線程的後續工作上會看到錯誤的上下文(為瞭解決這個問題,需要編寫良好的應用程式模型,它設置一個自定義上下文,在調用任何用戶代碼之前通常是要手動重設它)。甚至它發生在運行在相同的線程上,在此之前也需要一段時間,這種上下文在一段時間內不會被修複。如果它運行在不同的線程上,它最終將設置錯的上下文在這個線程。如此等等,非常不理想。

我正使用GetAwaiter().GetResult()。我還需要使用ConfigureAwait(false)嗎

不,ConfigureAwait 隻影響回調。特別是,awaiter 模式要求要求暴露一個 IsCompleted 屬性,GetResult 方法以及一個 OnCompleted 方法(可選擇是還有方法 UnsafeOnCompleted)。ConfigureAwait 隻影響 {Unsafe}OnCompleted 的行為,所以如果你只是直接調用 awaiter 的 GetResult 方法,無論你是在 TaskAwaiter 或是 ConfiguredTaskAwaitable.ConfiguredTaskAwaiter 做的任何事,這沒有任何不同。所以如果你在代碼中看到 task.ConfigureAwait(false).GetAwaiter().GetResult(),你可以用 task.GetAwaiter().GetResult() 替換(不過你還是得考慮你是否真的想阻塞它)。

我知道我在環境中運行,絕不會用到自定義同步上下文或任務調度器。那我能跳過使用ConfigureAwait(false)嗎

也許吧。它取決於你是如何保證“絕不”的。上一個FAQ需要註意的是,因為你正在工作的 app 模型不會設置自定義的同步上下文並且也不會在自定義的任務調度器上調用你的代碼,不意味著一些其他的用戶或庫代碼沒有這麼做。所以你得保證那中情況不會發生,或者至少估量它可能的風險。

我聽說在.NET Core 中ConfigureAwait(false) 已經不在必要了,是真的嗎

不。它還是需要的,當在.NET Core中它與在.NET Framework 運行需要的理由同樣明確。在這方面沒有任何改變。

但是,改變的是一些環境是否發佈了它們自己的同步上下文。特別是,在.NET Framework 的 ASP.NET 類有它自己的同步上下文,而.NET Core就沒有。那意思就是說,預設情況下運行在.NET Core 的代碼是不會看到自定義的同步上下文的,這運行在這樣的環境中就大大減少了 ConfigureAwait(false) 的需要。

但是,這不意味著永遠都不需要自定義的同步上下文或任務調度器。如果一些用戶代碼(或在你項目中使用的其他類庫代碼)設置了自定義同步上下文並且調用了你的代碼,或在一個被自定義調度器調度的任務中調用了你的代碼,那麼在 ASP.NET Core 中你的 await 也許就能看到非預設的上下文或調度器,這樣就會導致你要使用 ConfigureAwait(false)。當然,在這種情況下,如果你想避免同步阻塞(無論如何在你的應用程式中都應該這麼考慮)並且你不介意小的性能開銷,在這種受限的事情中,你儘可能的不要使用ConfigureAwait(false)

當在非同步流中使用 await foreach 時,我能使用 ConfigureAwait 嗎

當然。具體例子詳見 MSDN Magazine article

await foreach 綁定到了一個模式,當在一個非同步流 IAsyncEnumerable 時才能使用,它也能被用來迭代一些暴露正確的API錶面區域(surface area)。.NET 運行時庫包含了一個 ConfigureAwait 拓展方法在 IAsyncEnumerable 上,它返回一個自定義類型,這個類型包裝了 IAsyncEnumerable 和一個布爾值並且以正確的模式暴露。當編譯器生成對可枚舉的 MoveNextAsync 和 DisposeAsync 方法調用時,那些調用都會返回已配置的可枚舉結構類,並且它會以觸發配置的方式來執行等待。

await using 一個DisposeAsync對象時,能使用ConfigureAwait嗎

可以,儘管有點小麻煩。

在上個FAQ關於 IAsyncEnumerable 的描述,.NET 運行時類庫暴露一個 ConfigureAwait 拓展方法在 IAsyncDisposable 上,並且使用 await using 能很好的工作,它會以合適的模式實現(命名上,也暴露了合適的方法 DisposeAsync):

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

這裡的問題是,類型 c 現在還不是 MyAsyncDisposableClass,而是一個 System.Runtime.CompilerServices.ConfiguredAsyncDisposable,它是從 IAsyncDisposable 上的拓展方法 ConfigureAwait 返回的類型。

為瞭解決這個問題,你需要多寫一行:

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

現在這個 c 仍然是所需要的 MyAsyncDisposableClass。這也影響了,增加了 c 的範圍。如果這影響很大,你可以用大括弧把整個都包起來。

我已經用了ConfigureAwait(false),但是在等待後,我的AsyncLocal仍然流到了代碼中。這是bug嗎

不。這是意料之中的事。AsyncLocal 數據流是作為 ExecutionContext 的一部分,它是從 SynchronizationContext 獨立出來的。除非你顯式的使用xecutionContext.SuppressFlow()禁止 ExecutionContext,ExecutionContext(就是AsyncLocal 數據)將總會在等待中橫穿流動,無論是否使用了 ConfigureAwait 來避免捕捉原始同步上下文。更多信息詳盡這篇文章

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

庫作者有時候要表示他們需要使用 ConfigureAwait(false) 的失望,並要求使用侵入式更低的替代方法。

目前他們不需要,至少不需要構建到語言/編譯器/運行時。這裡有許多提議,對於這種情況所需要的方案,如

https://github.com/dotnet/csharplang/issues/645

https://github.com/dotnet/csharplang/issues/2542

https://github.com/dotnet/csharplang/issues/2649

https://github.com/dotnet/csharplang/issues/2746

如果這對你來說很重要,或者如果你有新的或更有趣的想法,我鼓勵你在這裡貢獻你新的想法討論。

註意

本人水平有限,肯定有蠻多翻譯不對的地方,我本來是想按照自己所理解的樣子去翻譯,但是又擔心距原意太大,所以儘可能的靠近字面意思翻譯(也算是自我學習,與英語練習吧)。還望各位多多包涵

想要瞭解這方面,我建議還是直接去看原文。

本文同步至:https://github.com/MarsonShine/MarsonShine.github.io/blob/master/mardown/async/ConfigureAwait-In-Deep.md


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 棧: 1、又名堆棧,它是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把 另一端稱為棧底。其特性是先進後出。 2、棧是線程私有的,生命周期跟線程相同,當創建一個線程時,同時會創建一個棧,棧的大小和深度都是固定的。 3、 方法參數列表中的變數,方法體中的基 ...
  • 這兩天看opencv-python的HSV色彩空間,在寫程式時發現用HSV來提取圖像區域是件令人噁心的麻煩事。拿閾值分割做個對比,閾值最多也就一兩個參數需要調整;但是HSV需要對三個通道調整上下限,也就是起碼有6個參數。於是乎,就一時興起決定做個小程式,把參數都做成滑動塊,這樣自然方便許多。一開始, ...
  • 一、什麼式方法區 方法區,也稱非堆(Non Heap),又是一個被線程共用的記憶體區域。其中主要存儲載入的類位元組碼、class/method/field等元數據對象、static final常量、static變數、jit編譯器編譯後的代碼等數據。另外,方法區包含了一個特殊的區域“運行時常量池”。 (1 ...
  • JVM體繫結構圖 Native Interface(本地介面) Java本地介面(Java Native Interface (JNI))允許運行在Java虛擬機(Java Virtual Machine (JVM))上的代碼調用本地程式和類庫,或者被它們調用,這些程式和類庫可以是其它語言編寫的,比 ...
  • 準備年後要跳槽,所以最近一直再看面試題,並且把收集到的面試題整理了以下發到博客上,希望對大家有所幫助。 首先是集合類的面試題 1. HashMap 排序題,上機題。 已知一個 HashMap<Integer,User>集合, User 有 name(String)和 age(int)屬性。請寫一個方 ...
  • 發現問題 在一次偶然中,在爬取某個網站時,老方法,打開調試工具查看請求方式,請求攔截,是否是非同步載入,不亦樂乎,當我以為這個網站非常簡單的時候,發現二級網頁的地址和源碼不對應 Ajax非同步載入?源碼也是這樣的 而且這些鏈接直... ...
  • 常用的軟體: 播放器: cloundMusic(網易雲音樂) https://music.163.com/#/download PotPlayer(一款強大的視頻播放器) https://daumpotplayer.com/download/ ACDsee(ACDsee圖片編輯器免費版) https ...
  • 想要實現二維數組中根據某個欄位排序,一般可以通過數組迴圈對比的方式實現。這裡介紹一種更簡單的方法,直接通過PHP函數實現。array_multisort() :可以用來一次對多個數組進行排序,或者根據某一維或多維對多維數組進行排序。詳細介紹可參考PHP手冊:https://www.php.net/m ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...