哈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 .
, ,
.