哈Ha! 我向您介绍Stephen Taub撰写
的ConfigureAwait FAQ文章的翻译。

Async /
await添加到七年前的.NET中。 这一决定不仅对.NET生态系统产生了重大影响-还反映在许多其他语言和框架中。 当前,.NET已在使用异步的其他语言构造方面实现了许多改进,已实现了具有异步支持的API,对基础架构也进行了根本性的改进,因此
async /
await工作原理类似于时钟(特别是性能和诊断功能已得到改进)在.NET Core中)。
ConfigureAwait是
async /
await一个方面,不断引起问题。 我希望我能回答其中的许多问题。 我将尝试使本文从头到尾都具有可读性,同时以对常见问题(FAQ)的回答的方式执行该文章,以便将来可以引用。
为了实际处理
ConfigureAwait ,我们将回顾一下。
什么是SynchronizationContext?
根据
System.Threading.SynchronizationContext文档,“提供了用于在各种同步模型中分发同步上下文的基本功能。” 这个定义不是很明显。
在99.9%的情况下,
SynchronizationContext仅用作带有虚拟
Post方法的类型,该方法接受委托以进行异步执行(
SynchronizationContext还有其他虚拟成员,但它们不那么常见,因此在本文中将不再讨论)。 基本类型的
Post方法实际上
只是调用 ThreadPool.QueueUserWorkItem以异步执行提供的委托。 派生类型将覆盖
Post以便委托可以在正确的时间在正确的位置执行。
例如,Windows窗体具有SynchronizationContext派生的
类型 ,该
类型重新定义了
Post以使其等效于
Control.BeginInvoke 。 这意味着对该
Post方法的任何调用都将导致在稍后阶段与对应的Control相关联的线程(即所谓的UI线程)中对委托的调用。 Windows窗体的核心是Win32消息处理。 消息循环在UI线程中执行,该线程仅等待处理新消息。 这些消息是由鼠标移动,单击,键盘输入,可由委托执行的系统事件等触发的。因此,如果您在Windows Forms应用程序中具有UI线程的
SynchronizationContext实例,则必须将委托传递给
Post方法才能在其中执行操作。
Windows Presentation Foundation(WPF)也具有从
SynchronizationContext派生的
类型 ,该
类型具有重写的
Post方法,该方法类似地使用WPF Dispatcher控件(而不是Windows Forms Control)将委托“定向”到UI流(使用
Dispatcher.BeginInvoke )。
Windows RunTime(WinRT)具有自己的
SynchronizationContext派生
类型 ,该
类型还使用
CoreDispatcher将委托放入UI线程
CoreDispatcher中。
这就是短语“ UI线程中的运行委托”的背后。 您还可以使用
Post方法和某些实现来实现
SynchronizationContext 。 例如,我不必担心委托在哪个线程中运行,但是我想确保
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,该API可用于按照编程人员希望的方式将委托排队以便执行,而无需了解实现细节。 假设我写了一个库,我需要在其中做一些工作,然后将委托排队回到原始上下文。 为此,我需要捕获其
SynchronizationContext ,完成所需的操作后,只需调用此上下文的
Post方法并将其传递给委托即可执行。 我不需要知道对于Windows窗体,您需要
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); } }); }
您可以使用
SynchronizationContext.SetSynchronizationContext方法从
Current属性中设置特殊上下文。
什么是任务计划程序?
SynchronizationContext是“调度程序”的通用抽象。 一些框架为此使用它们自己的抽象,并且
System.Threading.Tasks也不例外。 当
Task有可以排队和执行的委托时,它们与
System.Threading.Tasks.TaskScheduler关联。 还有一个用于排队委托的虚拟
Post方法(使用标准机制实现委托调用),
TaskScheduler提供抽象的
QueueTask方法(使用
ExecuteTask方法实现任务调用)。
返回
TaskScheduler.Default的默认调度程序。默认为线程池。 从
TaskScheduler ,还可以获取和覆盖用于设置
Task调用时间和地点的方法。 例如,核心库包括
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上下文中将要执行的任务
TaskScheduler 。
SynchronizationContext和TaskScheduler与await有什么关系?
假设您需要编写一个带有按钮的UI应用程序。 按下按钮将启动从网站的文本下载并将其设置为“
Content按钮。 该按钮只能从其所在的流的UI中进行访问,因此,当我们成功加载日期和时间并将其放置在按钮的
Content ,我们需要从对其进行控制的流中进行操作。 如果不满足此条件,我们将获得异常:
System.InvalidOperationException: ' , .'
我们可以手动使用
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; }
所有这一切都“起作用”并成功在UI线程中配置
Content ,因为在上述手动实现的版本中,默认情况下,等待任务是指
SynchronizationContext.Current和
TaskScheduler.Current 。 当您“期望” C#中的某些内容时,编译器将用于轮询(通过调用
GetAwaiter )的“预期”(在本例中为Task)的代码转换为“ waiting”(
TaskAwaiter )。 “等待”负责附加一个回调(通常称为“继续”),当等待完成时,该回调将返回状态机。 他使用在回调注册期间捕获的上下文/调度程序来实现此目的。 我们将进行一些优化和配置,如下所示:
object scheduler = SynchronizationContext.Current; if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default) { scheduler = TaskScheduler.Current; }
在这里,首先检查是否
SynchronizationContext了
SynchronizationContext ,如果没有
SynchronizationContext ,则检查是否
TaskScheduler非标准
TaskScheduler 。 如果有一个,则当回调准备好进行呼叫时,将使用捕获的调度程序;否则,将使用捕获的调度程序。 如果不是,则回调将作为完成预期任务的操作的一部分执行。
ConfigureAwait的作用(假)
ConfigureAwait方法不是特殊的:编译器或运行时不会以任何特定方式识别它。 这是返回结构的常规方法(
ConfiguredTaskAwaitable包装原始任务)并采用布尔值。 请记住,可以将
await与实现正确模式的任何类型一起使用。 如果返回另一种类型,则意味着编译器可以访问实例的
GetAwaiter方法(模式的一部分),但使用的是
ConfigureAwait返回的类型,而不是直接从任务执行。 这使您可以更改此特殊
await行为。
等待而不是等待
Task由
ConfigureAwait(continueOnCapturedContext: false)返回的类型
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用于从网络下载内容的库方法。 您调用此方法并同步阻塞,等待任务完成,例如,使用
.GetAwaiter()或
.GetAwaiter()或
.GetAwaiter() .GetResult() 。 现在考虑如果调用发生在当前
SynchronizationContext使用
MaxConcurrencySynchronizationContext显式地将其中的操作数限制为1时发生,或者隐式(如果它是具有单个线程(例如,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%的情况下,该值设置为false,
ConfigureAwait(false) 。
什么时候使用ConfigureAwait(false)?
这取决于您是实现应用程序级代码还是通用库代码。
在编写应用程序时,通常需要一些默认行为。 如果应用程序模型/环境(例如Windows窗体,WPF,ASP.NET Core)发布了特殊的
SynchronizationContext ,则几乎可以肯定有一个很好的理由:这意味着代码允许您处理同步上下文,以与应用程序模型/环境进行正确的交互。 例如,如果您在Windows Forms应用程序中编写事件处理程序,在xUnit中进行测试,或者在ASP.NET MVC控制器中编写代码,则不管应用程序模型是否已发布
SynchronizationContext ,如果都存在,都需要使用
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);
这将导致不当行为。 这同样适用于依赖于
HttpContext.Current的经典ASP.NET应用程序中的代码。 当使用
ConfigureAwait(false)随后尝试使用
Context.Current函数可能会导致问题。
这就是通用库的区别。 它们之所以具有通用性,部分原因是它们不关心使用它们的环境。 您可以从Web应用程序,客户端应用程序或测试中使用它们-没关系,因为库代码对于可以使用它的应用程序模型是不可知的。 不可知论还意味着该库将不做任何事情来与应用程序模型进行交互,例如,它将无法访问用户界面控件,因为通用库对此一无所知。 由于不需要在任何特定环境中运行代码,因此我们可以避免将强制执行/回调强制到原始上下文,而我们可以使用
ConfigureAwait(false)来执行此操作,这将为我们带来性能优势并提高可靠性。 这将导致以下情况:
如果要编写通用库代码,请使用ConfigureAwait(false) 。 这就是.NET Core运行时库中每个(或几乎每个)等待都使用ConfigureAwait(false)的原因。 除少数例外(很可能是错误)外,它们将得到修复。 ,
PR ConfigureAwait(false) HttpClient .
. , (, , , ) , API, . , , ” " . , , Where LINQ:
public static async IAsyncEnumerable<T> WhereAsync(this IAsyncEnumerable<T> source, Func<T, bool> predicate) .
predicate SynchronizationContext ?
WhereAsync , ,
ConfigureAwait(false) .
:
ConfigureAwait(false) /app-model-agnostic .
ConfigureAwait (false), ?
, , . ,
await . , , . , , , ,
ConfigureAwait(false) , , .
ConfigureAwait (false) , — ?
, . FAQ.
await task.ConfigureAwait(false) , ( ),
ConfigureAwait(false) , - , .
,
await , ,
SynchronizationContext TaskScheduler . ,
CryptoStream .NET , .
awaiter确保第一次等待后的代码在线程池线程中执行。但是,即使在这种情况下,您也会注意到下次等待仍在使用ConfigureAwait(false); 从技术上讲,这不是必需的,但是由于不需要了解为什么不使用它,因此可以大大简化代码审查ConfigureAwait(false)。是否可以使用Task.Run避免使用ConfigureAwait(false)?
是的,如果您写: Task.Run(async delegate { await SomethingAsync();
ConfigureAwait(false) SomethingAsync() , ,
Task.Run , ,
SynchronizationContext.Current null . ,
Task.Run TaskScheduler.Default ,
TaskScheduler.Current Default . ,
await ,
ConfigureAwait(false) . , . :
Task.Run(async delegate { SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx()); await SomethingAsync();
SomethingAsync SynchronizationContext.Current SomeCoolSyncCtx .
await , SomethingAsync . , , , , , .
/ . / .
, , , . , ,
ConfigureAwait(false) CA2007 . ,
ConfigureAwait , , . , , - , ,
ConfigureAwait(false) .
SynchronizationContext.SetSynchronizationContext, ConfigureAwait (false)?
不行 , .
:
Task t; SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { t = CallCodeThatUsesAwaitAsync();
,
CallCodeThatUsesAwaitAsync null . . ,
await TaskScheduler.Current .
TaskScheduler ,
await '
CallCodeThatUsesAwaitAsync TaskScheduler .
Task.Run FAQ, : ,
try , ( ).
:
SynchronizationContext old = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); try { await t; } finally { SynchronizationContext.SetSynchronizationContext(old); }
? , . , / . ,
SynchronizationContext , , . , , , , . , , . , . 依此类推。 .
如果我使用GetAwaiter().GetResult(),是否需要使用ConfigureAwait(false)?
不行
ConfigureAwait仅影响回调。特别是,模板awaiter要求您awaiter提供属性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没有。这意味着默认情况下SynchronizationContext,ASP.NET Core应用程序中运行的代码将看不到特殊代码,从而减少了ConfigureAwait(false)对此环境的需求。但是,这并不意味着永远不会有习惯SynchronizationContext或TaskScheduler . - ( , ) , ,
await ' ASP.NET Core ,
ConfigureAwait(false) . , , ( -) ,
ConfigureAwait(false) .
ConfigureAwait, « foreach» IAsyncEnumerable?
是的 .
MSDN .
Await foreach , ,
IAsyncEnumerable<T> . , API. .NET
ConfigureAwait IAsyncEnumerable<T> , ,
IAsyncEnumerable<T> Boolean .
MoveNextAsync DisposeAsync . , , .
ConfigureAwait, 'await using' IAsyncDisposable?
, .
IAsyncEnumerable<T> , .NET
ConfigureAwait IAsyncDisposable await using , , ( ,
DisposeAsync ):
await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false)) { ... }
,
c —
MyAsyncDisposableClass ,
System.Runtime.CompilerServices.ConfiguredAsyncDisposable ,
ConfigureAwait 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) .
, , // . , , :
1 ,
2 ,
3 ,
4 .
, ,
.