原文: "https://devblogs.microsoft.com/dotnet/configureawait faq/" .NET 在七多年前在語言和類庫添加了 。在那個時候,它像野火一樣流行,不僅遍及.NET生態系統,而且還可以以多種其他語言和框架進行複製。在利用非同步的其他語言構造,提供非同步 ...
原文: https://devblogs.microsoft.com/dotnet/configureawait-faq/
.NET 在七多年前在語言和類庫添加了 async/await
。在那個時候,它像野火一樣流行,不僅遍及.NET生態系統,而且還可以以多種其他語言和框架進行複製。在利用非同步的其他語言構造,提供非同步支持的API以及進行async/ await相關的基礎架構方面的基本改進方面,.NET也實現了很多改進(特別是.NET Core的性能和支持診斷的改進) 。
但是,async/ await
依舊引起疑問的一個方面是ConfigureAwait
在這篇文章中,我希望回答其中的許多問題。我希望這篇文章從頭到尾都是可讀的,並且是可以用作將來參考的常見問題解答(FAQ)列表。
要真正理解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線程上運行“消息迴圈”,該線程只是等待新消息到達以進行處理。這些消息可能用於滑鼠移動和單擊,用於鍵盤鍵入,用於系統事件,可供可調用的委托等。因此,給定SynchronizationContext
Windows Forms應用程式的UI線程的實例,以使委托在其上執行UI線程,只需要將其傳遞給Post
。
Windows Presentation Foundation(WPF)也是如此。它具有自己的SynchronizationContext派生類型,並帶有Post
覆蓋,該覆蓋類似地(通過Dispatcher.BeginInvoke
)“封送” UI線程的委托,在這種情況下,由WPF Dispatcher而不是Windows Forms Control管理。
對於Windows運行時(WinRT)。它具有自己的SynchronizationContext派生類型並帶有Post
重寫,該重寫也通過將該隊列排隊到UI線程CoreDispatcher
。
這超出了“在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,可用於將委托排隊,以處理實現的創建者所希望的,而無需瞭解該實現的細節。因此,如果我正在編寫一個庫,並且想開始做一些工作,然後將一個代表排隊回到原始位置的“上下文”,則只需要抓住它們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 Task
在Task
計划到運行時將不運行ExclusiveScheduler
,一次只能Task
運行一個互斥對象…這樣,它的行為非常類似於讀/寫鎖。
類似SynchronizationContext
,TaskScheduler
還具有一個Current
屬性,該屬性返回“當前的TaskScheduler
。不類似於SynchronizationContext
,然而,這沒有設置當前調度程式的方法。相反,當前調度程式是與當前正在運行Task的調度程式相關聯的調度程式,並且調度程式作為啟動的一部分提供給系統Task
。因此,舉例來說,使用這個程式將輸出“True”,使用lambda在StartNew上執行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
在SynchronizationContext.Current
返回的內容上排隊運行Post
。
SynchronizationContext和TaskScheduler與等待如何關聯?
考慮在UI應用使用編Button
。在點擊Button
,我們要下載從網站一些文字,並將其設置為Button
的Content
。該Button
只應該從擁有它的UI線程訪問,所以當我們已經成功地下載了新的日期和時間文本和想把它存回Button
的Content
,我們需要從擁有控制線程這樣做。如果不這樣做,則會出現類似以下的異常:
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)在這種環境中運行的需要。
但是,這並不意味著永遠不會有習俗SynchronizationContext
或TaskScheduler
禮物。如果某些用戶代碼(或您的應用程式正在使用的其他庫代碼)設置了自定義上下文並調用了您的代碼,或者按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。除非您顯式禁用ExecutionContext
了ExecutionContext.SuppressFlow()
,否則ExecutionContext
(,因此AsyncLocal<T>
數據)始終將流經awaits
,無論是否ConfigureAwait
用於避免捕獲原始SynchronizationContext。有關更多信息,請參閱此博客文章。
該語言可以幫助我避免在我的庫中顯式使用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。
如果這對您很重要,或者您覺得這裡有新的有趣的想法,我鼓勵您為這些或新的討論貢獻自己的想法。