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來做任何事,例如,我也許不關心執行緒運行委托所做的事,但是我想確保所有在我撰寫的SynchronizationContext的方法 Post 都能以一定程度的并發度執行,我可以實作這樣一個自定義的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),而不需要知道具體實作的細節,
所以,如果我們在撰寫類別庫的時候,并且想要進行和執行相同的作業,那么就排隊委托給原來位置的“背景關系”,那么我就只需要獲取這個“同步背景關系”,并占有它,然后當完成我的作業時呼叫這個背景關系中的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方法,(A framework that wants to expose a custom context from Current uses the SynchronizationContext.SetSynchronizationContext method.)
什么是TaskScheduler
對于“調度器”,SynchronizationContext是一個抽象類,并且個別的框架有時候擁有自己的抽象,System.Threading.Task也不例外,當任務被那些排隊及執行的委托支持(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`任務將不會執行,一次只運行一個獨立任務... 這樣的話,它行為就很像一個讀寫鎖,
像 SynchronizationContext,TaskScheduler 都有一個 Current 屬性,它會回傳一個“current” Taskscheduler,而不像 SynchronizationContext,這里不存在方法可以設定當前調度器,相反,當前的調度器是一個與當前正在運行的任務相關,并且這個調度器作為啟動任務的一部分提供給給系統,例如下面這個程式將會輸出“True”,與 StartNew 一起使用的lambda在 ConcurrentExclusiveSchedulerPair 的 ExclusiveScheduler 方法上呼叫,并且將會看到 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 task和await 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(); // 在這里不會看到原始背景關系
}
finally { SynchronizationContext.SetSynchronizationContext(old); }
await t; // 仍然會得到原始背景關系
我們希望看到在 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
當await using 一個DisposeAsync物件時,能使用ConfigureAwait嗎
可以,盡管有點小麻煩,
在上個FAQ關于 IAsyncEnumerable
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 來說也是有影響的,它增加了 c 的范圍,如果你介意的話,你可以用大括號把整個都包起來,
我已經用了ConfigureAwait(false),但是在await后,AsyncLocal仍然流到了代碼中,這是bug嗎
不,這是意料之中的事,AsyncLocalExecutionContext.SuppressFlow()來禁止 ExecutionContext,否則無論你是否使用了 ConfigureAwait 來避免捕捉原始同步背景關系, ExecutionContext(就是AsyncLocal
語言能幫助我在庫中避免顯式使用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
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/2961.html
標籤:ASP.NET
