.NET ConfigureAwait FAQ (翻譯)

来源:https://www.cnblogs.com/Starts_2000/p/18310972
-Advertisement-
Play Games

閱讀了 https://devblogs.microsoft.com/dotnet/configureawait-faq/,感覺其對於 .NET 非同步編程有非常有意義的指導,對於進一步學習和理解 .NET 非同步編程非常友邦做,所以進行翻譯以供參考學習。 七年多前,.NET 在語言和庫中加入了 asy ...


  閱讀了 https://devblogs.microsoft.com/dotnet/configureawait-faq/,感覺其對於 .NET 非同步編程有非常有意義的指導,對於進一步學習和理解 .NET 非同步編程非常友邦做,所以進行翻譯以供參考學習。

 

  七年多前,.NET 在語言和庫中加入了 async/await 。在這段時間里,它像野火一樣燎原,不僅在 .NET 生態系統中流行開來,還被無數其他語言和框架所效仿。在 .NET 中,它也得到了大量的改進,包括利用非同步的附加語言構造、提供非同步支持的 API,並從根本上改進了 async/await 運行的基礎架構(特別是 .NET Core 中的性能和診斷功能改進)。

  不過,async/await 的一個方面仍然存在問題,那就是 ConfigureAwait。在本篇文章中,我希望能回答其中的許多問題。我希望這篇文章從頭到尾都具有可讀性,同時也是一個常見問題列表(FAQ),可供今後參考。

  要真正瞭解 ConfigureAwait,我們需要從更早的時候開始...

什麼是 SynchronizationContext?

  System.Threading.SynchronizationContext 文檔指出,它 “為在各種同步模型中傳播同步上下文提供了基本功能”。這樣的描述並不明顯。

  對於 99.9% 的用例來說,SynchronizationContext 只是一個提供虛擬 Post 方法的類型,該方法接收一個非同步執行的委托(SynchronizationContext 上還有其他各種虛擬成員,但它們用得很少,與本討論無關)。基礎類型的 Post 字面上只是調用 ThreadPool.QueueUserWorkItem 來非同步調用所提供的委托。但是,派生類型會覆蓋 Post,以便在最合適的時間、最合適的地點執行委托。

  例如,Windows 窗體有一個 SynchronizationContext 派生類型,該類型重載了 Post,使其與 Control.BeginInvoke 的功能等效;這意味著對其 Post 方法的任何調用都將導致委托在稍後的某個時刻在與相關控制項(又稱 “UI 線程”)關聯的線程上被調用。Windows 窗體依賴於 Win32 消息處理,併在用戶界麵線程上運行一個 “消息迴圈”,該線程只需等待新消息的到來即可進行處理。這些消息可能是滑鼠移動和點擊、鍵盤輸入、系統事件、可調用的委托等。因此,如果給定了 Windows 窗體應用程式 UI 線程的 SynchronizationContext 實例,要在 UI 線程上執行委托,只需將其傳遞給 Post 即可。

  Windows Presentation Foundation(WPF)也是如此。它有自己的 SynchronizationContext 派生類型,其中的 Post 重載同樣可以將委托 “marshals ”到 UI 線程(通過 Dispatcher.BeginInvoke),在這種情況下,委托是由 WPF Dispatcher 而不是 Windows Forms 控制項管理的。

  而對於 Windows RunTime(WinRT)。它有自己的 SynchronizationContext 派生類型,具有 Post 覆蓋功能,還能通過其 CoreDispatcher 將委托隊列到 UI 線程。

這不僅僅是 “在用戶界麵線程上運行此委托”。任何人都可以實現一個帶有 Post 的同步上下文(SynchronizationContext),它可以做任何事情。例如,我可能並不關心委托在哪個線程上運行,但我希望確保任何 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,可用於對委托進行隊列,以便按照實現創建者的意願進行處理,而無需瞭解該實現的細節。因此,如果我正在編寫一個庫,而我想去做一些工作,然後將一個委托隊列回原始位置的 “上下文”,我只需要抓取它們的 SynchronizationContext,並將其保留下來,然後當我完成我的工作時,在該上下文上調用 Post 來移交我想調用的委托。我不需要知道,對於 Windows 窗體,我應該抓取一個控制項並使用它的 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); }
    });
}

  希望從當前環境公開自定義上下文的框架會使用 SynchronizationContext.SetSynchronizationContext 方法。

什麼是任務調度器?

  SynchronizationContext 是 “調度程式 ”的一般抽象。個別框架有時會有自己的調度程式抽象,System.Threading.Tasks 也不例外。當任務由委托支持,可以排隊和執行時,它們就與 System.Threading.Tasks.TaskScheduler 關聯。正如 SynchronizationContext 提供了一個虛擬的 Post 方法來對委托的調用進行排隊(實現隨後通過典型的委托調用機制調用委托),TaskScheduler 也提供了一個抽象的 QueueTask 方法(實現隨後通過 ExecuteTask 方法調用該任務)。

  TaskScheduler.Default 返回的預設調度程式是線程池,但也可以派生自 TaskScheduler 並重寫相關方法,以實現在何時何地調用任務的任意行為。例如,核心庫包括 System.Threading.Tasks.ConcurrentExclusiveSchedulerPair 類型。該類的實例公開了兩個 TaskScheduler 屬性,一個稱為 ExclusiveScheduler,另一個稱為 ConcurrentScheduler。調度到 ConcurrentScheduler 的任務可以併發運行,但必須遵守在構建 ConcurrentExclusiveSchedulerPair 時提供給它的限制(類似於前面顯示的 MaxConcurrencySynchronizationContext),而且當調度到 ExclusiveScheduler 的任務運行時,ConcurrentScheduler 任務不會運行,每次只允許運行一個獨占任務......這樣,它的行為就非常像讀寫鎖。

  與 SynchronizationContext 一樣,TaskScheduler 也有一個 Current 屬性,用於返回 “當前 ”TaskScheduler。但與 SynchronizationContext 不同的是,沒有設置當前調度程式的方法。當前調度程式是與當前運行的任務相關聯的調度程式,而調度程式是作為啟動任務的一部分提供給系統的。因此,舉例來說,這個程式將輸出 “True”,因為 StartNew 使用的 lambda 是在 ConcurrentExclusiveSchedulerPair 的 ExclusiveScheduler 上執行的,並且會看到 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 方法,該方法可創建一個新的 TaskScheduler,使用其 Post 方法對任務進行排隊,以便在 SynchronizationContext.Current 返回的任務上排隊運行。

SynchronizationContext 和 TaskScheduler 與 await 有什麼關係?

  考慮編寫一個帶有按鈕的 UI 應用程式。點擊按鈕後,我們希望從一個網站下載一些文本,並將其設置為按鈕的內容。按鈕只能從擁有它的用戶界麵線程中訪問,因此當我們成功下載了新的日期和時間文本並想將其存儲回按鈕的內容時,我們需要從擁有控制項的線程中進行操作。否則就會出現以下異常:

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

  如果是手動編寫,我們可以使用 SynchronizationContext(如前所述)將 “內容 ”的設置傳送回原始上下文,例如通過 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;
}

  這就 “just works”,成功地在 UI 線程上設置了內容,因為就像上面手動實現的版本一樣,等待任務預設會關註 SynchronizationContext.Current 和 TaskScheduler.Current。在 C# 中等待任何任務時,編譯器會轉換代碼以詢問(通過調用 GetAwaiter)“awaitable”(此處為任務)“awaiter”(此處為 TaskAwaiter<string>)。該等待者負責連接回調(通常稱為 “繼續”),當等待對象完成時,回調將回調到狀態機,並使用回調註冊時捕獲的上下文/調度程式來完成。雖然所使用的代碼並不完全相同(還進行了額外的優化和調整),但差不多是這樣的:

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

  換句話說,它會首先檢查是否設置了同步上下文(SynchronizationContext),如果沒有,則檢查是否存在非預設的任務調度程式(TaskScheduler)。如果找到了,當回調準備好被調用時,它就會使用捕獲的調度程式;否則,它一般只會在完成等待任務的操作中執行回調。

ConfigureAwait(false) 的作用是什麼?

  ConfigureAwait 方法並不特殊:編譯器或運行時都不會以任何特殊方式識別它。它只是一個返回結構體(ConfiguredTaskAwaitable)的方法,該結構體封裝了調用它的原始任務以及指定的布爾值。請記住,await 可以用於任何暴露正確模式的類型。通過返回不同的類型,這意味著當編譯器訪問實例 GetAwaiter 方法(模式的一部分)時,它是根據 ConfigureAwait 返回的類型而不是直接根據任務來訪問的,這就提供了一個鉤子,可以通過這個自定義的 awaiter 來改變 await 的行為方式。

  具體來說,等待從 ConfigureAwait 返回的類型(continueOnCapturedContext: false)而不是直接等待任務,最終會影響前面所示的如何捕獲目標上下文/調度程式的邏輯。這實際上使之前顯示的邏輯變得更像這樣:

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() 來關閉返回的任務對象。現在考慮一下,如果您在當前同步上下文(SynchronizationContext)中調用該方法,而當前同步上下文將其上可運行的操作數量限製為 1,無論是顯式地通過類似前面所示的 MaxConcurrencySynchronizationContext,還是隱式地通過只有一個線程可使用的上下文(如 UI 線程),都會發生什麼情況。因此,我們在這一個線程上調用方法,然後阻塞它,等待操作完成。該操作啟動網路下載並等待下載。預設情況下,等待任務會捕獲當前的 SynchronizationContext,因此它會捕獲當前的 SynchronizationContext,當網路下載完成後,它會將調用剩餘操作的回調隊列回 SynchronizationContext。但是,唯一能處理排隊回調的線程目前正被你的代碼阻塞,等待操作完成。而在處理回調之前,該操作不會完成。死鎖!即使上下文沒有將併發限製為 1,但當資源以任何方式受到限制時,也會出現這種情況。想象一下同樣的情況,只不過使用的是最大併發同步上下文(MaxConcurrencySynchronizationContext),其限製為 4。 我們並沒有隻調用一次操作,而是向該上下文排隊調用了 4 次,每次調用後都會阻塞,等待調用完成。現在,在等待非同步方法完成時,我們仍然阻塞了所有資源,而唯一能讓這些非同步方法完成的條件是,它們的回調能被這個已經完全消耗掉的上下文處理。這又是一個死鎖!如果庫方法使用了 ConfigureAwait(false),就不會將回調排隊返回到原始上下文,從而避免了死鎖情況。

為什麼要使用 ConfigureAwait(true)?

  你不會這麼做的,除非你純粹是為了表明你故意不使用 ConfigureAwait(false)(例如為了消除靜態分析警告或類似警告)。ConfigureAwait(true)沒有任何意義。在比較 await task 和 await task.ConfigureAwait(true) 時,它們在功能上是相同的。如果你在生產代碼中看到 ConfigureAwait(true),可以刪除它,不會有任何不良影響。

  ConfigureAwait 方法接受一個布爾值,因為在某些特殊情況下,你需要傳遞一個變數來控制配置。但 99% 的使用情況是使用硬編碼的 false 參數值,即 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;
}

  將導致不良行為。依賴 HttpContext.Current 的經典 ASP.NET 應用程式中的代碼也是如此;使用 ConfigureAwait(false),然後嘗試使用 HttpContext.Current 很可能會導致問題。

  相比之下,通用庫之所以 “通用”,部分原因在於它們不關心使用環境。您可以在網路應用程式、客戶端應用程式或測試中使用它們,這並不重要,因為庫代碼與可能使用的應用程式模型無關。不可知性還意味著它不會做任何需要以特定方式與應用程式模型交互的事情,例如,它不會訪問 UI 控制項,因為通用庫對 UI 控制項一無所知。既然我們不需要在任何特定環境中運行代碼,我們就可以避免將續程/回調強制返回到原始上下文,我們可以通過使用 ConfigureAwait(false)來做到這一點,並獲得其帶來的性能和可靠性優勢。這就引出了一個普遍的指導原則:如果你正在編寫通用庫代碼,請使用 ConfigureAwait(false)。舉例來說,這就是為什麼您會看到 .NET Core 運行時庫中的每一個(或幾乎每一個)await 都在每一個 await 上使用 ConfigureAwait(false);除了少數例外情況,如果不使用 ConfigureAwait(false),則很可能是需要修複的錯誤。例如,這個 PR 修複了 HttpClient 中一個缺失的 ConfigureAwait(false) 調用。

  當然,與所有指南一樣,也會有例外情況,在某些地方它並不合理。例如,在通用程式庫中,一個較大的例外情況(或至少是需要考慮的類別)是,這些程式庫的 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) 之後的代碼不會在原始上下文中運行。這是因為已完成的 awaitables 上的 await 只是同步運行過 await,而不是強制將任何內容排隊返回。因此,如果你等待一個在等待時已經完成的任務,無論你是否使用了 ConfigureAwait(false),緊隨其後的代碼都將繼續在當前線程的任何上下文中執行。

只在我的方法中的第一個 await 上使用 ConfigureAwait(false),而不在其他 await 上使用,這樣可以嗎?

  一般來說,不會。請參見前面的常見問題。如果 await 任務.ConfigureAwait(false)涉及的任務在等待時已經完成(這種情況實際上非常常見),那麼 ConfigureAwait(false) 就沒有意義了,因為線程會繼續在之後的方法中執行代碼,並且仍在之前的相同上下文中。

  一個值得註意的例外情況是,如果你知道第一個 await 將始終非同步完成,並且被等待的事物將在沒有自定義 SynchronizationContext 或 TaskScheduler 的環境中調用其回調。例如,.NET 運行時庫中的 CryptoStream 希望確保其潛在的計算密集型代碼不會作為調用者同步調用的一部分運行,因此它使用了自定義 awaiter,以確保第一個等待之後的所有內容都線上程池線程上運行。不過,即使在這種情況下,你也會註意到下一個 await 仍然使用了 ConfigureAwait(false);從技術上講,這並不是必須的,但它讓代碼審查變得更容易,因為否則每次查看這段代碼時,就不需要分析為什麼不使用 ConfigureAwait(false)了。

能否使用 Task.Run 來避免使用 ConfigureAwait(false)?

  是的,如下示例代碼:

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

  在 SomethingAsync() 上調用 ConfigureAwait(false) 將是無效的,因為傳遞給 Task.Run 的委托將線上程池線程上執行,堆棧上沒有更高的用戶代碼,因此 SynchronizationContext.Current 將返回空值。此外,Task.Run 還隱式地使用了 TaskScheduler.Default,這意味著在委托中查詢 TaskScheduler.Current 也將返回 Default。這意味著無論是否使用了 ConfigureAwait(false),await 都將表現出相同的行為。此外,它也不保證該 lambda 內部的代碼會做什麼。如下代碼:

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

  SomethingAsync 中的代碼實際上就會將 SynchronizationContext.Current 視為 SomeCoolSyncCtx 實例,並且該等待和 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 中的代碼將當前上下文視為空。確實如此。因此,如果這段代碼運行在某個自定義的 TaskScheduler 上,CallCodeThatUsesAwaitAsync 中的等待(且未使用 ConfigureAwait(false))仍將看到並隊列回該自定義 TaskScheduler。

  所有註意事項與之前的 Task.Run 相關常見問題解答中的一樣:這種變通方法會產生影響,而且 try 內的代碼也可以通過設置不同的上下文(或調用非預設 TaskScheduler 的代碼)來挫敗這些嘗試。

  對於這種模式,您還需要註意一個細微的變化:

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

  看到問題所在了嗎?這有點難看,但也有可能造成很大影響。我們無法保證 await 最終會在原始線程上調用回調/繼續,這意味著將 SynchronizationContext 重置回原始線程可能不會真正發生在原始線程上,這可能會導致該線程上的後續工作項看到錯誤的上下文(為瞭解決這個問題,編寫良好的應用程式模型在設置自定義上下文時通常會添加代碼,以便在調用任何進一步的用戶代碼前手動重置上下文)。即使它碰巧運行在同一線程上,也可能要過一段時間才能運行,因此上下文在一段時間內不會得到適當恢復。如果運行在不同的線程上,最終可能會將錯誤的上下文設置到該線程上。諸如此類,不一而足。這與理想狀態相去甚遠。

我正在使用 GetAwaiter().GetResult(),我需要使用 ConfigureAwait(false) 嗎?

  ConfigureAwait 隻影響回調。具體來說,awaiter 模式要求 awaiter 公開 IsCompleted 屬性、GetResult 方法和 OnCompleted 方法(可選擇 UnsafeOnCompleted 方法)。ConfigureAwait 隻影響 {Unsafe}OnCompleted 的行為,因此如果你只是直接調用 awaiter 的 GetResult() 方法,那麼無論是在 TaskAwaiter 還是在 ConfiguredTaskAwaitable.ConfiguredTaskAwaiter 上進行調用,行為上都不會有任何區別。因此,如果您在代碼中看到 task.ConfigureAwait(false).GetAwaiter().GetResult(),您可以將其替換為 task.GetAwaiter().GetResult()(同時也要考慮您是否真的想這樣阻塞)。

我知道我運行的環境永遠不會有自定義同步上下文或自定義任務調度程式,我可以不使用 ConfigureAwait(false)嗎?

  也許吧,這取決於你對 “從不 ”這部分有多大把握。正如之前的常見問題中提到的,您正在使用的應用程式模型沒有設置自定義同步上下文,也沒有在自定義任務調度程式上調用您的代碼,但這並不意味著其他用戶或庫代碼不會這樣做。因此,您需要確保情況並非如此,或者至少認識到可能存在的風險。

我聽說在 .NET Core 中不再需要 ConfigureAwait(false),是真的嗎?

  錯。在 .NET Core 上運行時需要它,原因與在 .NET Framework 上運行時完全相同。這方面沒有任何變化。

  不過,變化的是某些環境是否發佈了自己的 SynchronizationContext。特別是,.NET Framework 上的經典 ASP.NET 有自己的 SynchronizationContext,而 ASP.NET Core 則沒有。這意味著在 ASP.NET Core 應用程式中運行的代碼預設不會看到自定義的 SynchronizationContext,從而減少了在這種環境中運行 ConfigureAwait(false) 的需要。

  但這並不意味著永遠不會出現自定義 SynchronizationContext 或 TaskScheduler。如果某些用戶代碼(或您的應用程式使用的其他庫代碼)設置了自定義上下文並調用了您的代碼,或者在調度到自定義 TaskScheduler 的任務中調用了您的代碼,那麼即使在 ASP.NET Core 中,您的等待也可能會看到非預設上下文或調度器,從而導致您想要使用 ConfigureAwait(false)。當然,在這種情況下,如果您避免同步阻塞(在 Web 應用程式中無論如何都應避免這樣做),如果您不介意在這種有限情況下的少量性能開銷,您可能可以不使用 ConfigureAwait(false)。

在對 IAsyncEnumerable 進行 “await foreach ”時,能否使用 ConfigureAwait?

  是的,請參閱 MSDN Magazine 這篇文章中的示例。

  await foreach 與一種模式綁定,因此,雖然它可以用於枚舉 IAsyncEnumerable<T>,但也可以用於枚舉暴露正確 API 錶面區域的東西。.NET運行時庫包含一個關於 IAsyncEnumerable<T> 的 ConfigureAwait 擴展方法,該方法返回一個封裝了 IAsyncEnumerable<T> 和布爾值的自定義類型,並公開了正確的模式。當編譯器生成對枚舉器的 MoveNextAsync 和 DisposeAsync 方法的調用時,這些調用會指向返回的已配置枚舉器結構類型,並以所需的配置方式執行等待。

在 “等待使用” IAsyncDisposable 時,能否使用 ConfigureAwait?

  是的,不過有一個小麻煩。

  與上一個常見問題中描述的 IAsyncEnumerable<T> 一樣,.NET 運行時庫在 IAsyncDisposable 上公開了一個 ConfigureAwait 擴展方法,await 使用者只要實現了適當的模式(即公開了一個適當的 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 仍在 await 之後流向代碼,這是一個錯誤嗎?

  不,這是預料之中的。AsyncLocal<T> 數據作為 ExecutionContext 的一部分流動,而 ExecutionContext 與 SynchronizationContext 是分開的。除非您使用 ExecutionContext.SuppressFlow() 顯式禁用了 ExecutionContext 流量,否則 ExecutionContext(以及 AsyncLocal<T> 數據)將始終在等待中流動,無論是否使用了 ConfigureAwait 來避免捕獲原始的 SynchronizationContext。更多信息,請參閱本博文

.NET(C#)能否幫助我避免在庫中明確使用 ConfigureAwait(false)?

  庫開發人員有時會對需要使用 ConfigureAwait(false) 表示不滿,並要求提供侵入性較小的替代方法。

  目前還沒有任何替代方案,至少沒有內置在語言/編譯器/運行時中。不過,關於這種解決方案的建議有很多,例如:

  1. https://github.com/dotnet/csharplang/issues/645
  2. https://github.com/dotnet/csharplang/issues/2542
  3. https://github.com/dotnet/csharplang/issues/2649 
  4. https://github.com/dotnet/csharplang/issues/2746

  如果這對您很重要,或者您覺得自己有新的有趣的想法,我鼓勵您在這些討論或新的討論中發表您的想法。


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

-Advertisement-
Play Games
更多相關文章
  • 前言 本人在配置VsCode C++開發環境時,查看了很多的博客,內容參差不齊,尤其是關於json文件的配置,繞得人頭很暈,最終還是通過閱讀官方文檔,結合部分博客的指引,完成了環境的配置,在此記錄本人的配置過程,希望能幫助到大家。事先聲明,本文的內容大量引自Vs Code官方的文章:https:// ...
  • 一:背景 1. 講故事 最新版本 1.2402.24001.0 的WinDbg真的讓人很興奮,可以將自己偽裝成 GDB 來和遠程的 GDBServer 打通來實現對 Linux 上 .NET程式進行調試,這樣就可以繼續使用熟悉的WinDbg 命令,在這個版本中我覺得 WinDbg 不再是 WinDb ...
  • 前置 預先連接 可以從連接器創建預先連接,並可以放置在ItemContainer或Connector上(如果AllowOnlyConnectors為false)。 預先連接的Content可以使用ContentTemplate進行自定義。如果EnablePreview為true,PreviewTar ...
  • 現如今大模型遍地都是,OpenAI的ChatGPT,Meta的Llama(羊駝),Anthropic的Claude,Microsoft的Phi3,Google的Gemini...... 怎麼都是國外的???嗯,國內也有,比如騰訊有混元大模型,位元組跳動有豆包大模型等等。 不過這不是今天的重點,這寫國內 ...
  • 前兩天發了一篇關於模式匹配的文章,鏈接地址,有小伙伴提到使用.NET6沒法體驗 C#新特性的疑問, 其實呢只要本地的SDK源代碼編譯器能支持到的情況下(直接下載VS2022或者VS的最新preview版本) 只需要做很小的改動就可以支持的. 目前仍然還有一些小伙伴因為歷史原因可能還在寫.NET Fr ...
  • 字元串轉換為數字int.TryParse() bool success = int.TryParse("300",out int b); Console.WriteLine(success); // 輸出為 true Console.WriteLine(b); //輸出為 300 字元串里的“300 ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • AutoFixture是一個.NET庫,旨在簡化單元測試中的數據設置過程。通過自動生成測試數據,它幫助開發者減少測試代碼的編寫量,使得單元測試更加簡潔、易讀和易維護。AutoFixture可以用於任何.NET測試框架,如xUnit、NUnit或MSTest。 預設情況下AutoFixture生成的字 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...