ConfigureAwait:常见问题

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

图片

Async / await添加到七年前的.NET中。 这一决定不仅对.NET生态系统产生了重大影响-还反映在许多其他语言和框架中。 当前,.NET已在使用异步的其他语言构造方面实现了许多改进,已实现了具有异步支持的API,对基础架构也进行了根本性的改进,因此async / await工作原理类似于时钟(特别是性能和诊断功能已得到改进)在.NET Core中)。

ConfigureAwaitasync / 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属性: ExclusiveSchedulerConcurrentScheduler 。 可以并行执行在ConcurrentScheduler调度的任务,但要考虑到ConcurrentExclusiveSchedulerPair创建时设置的限制(类似于MaxConcurrencySynchronizationContext )。 如果在ExclusiveScheduler执行任务并且一次仅允许运行一个独占任务,则不会执行ConcurrentScheduler任务。 此行为与读/写锁非常相似。

SynchronizationContext一样, TaskScheduler具有Current属性,该属性返回当前TaskScheduler 。 但是,与SynchronizationContext不同,它缺少一种用于设置当前调度程序的方法。 而是,调度程序与当前任务相关联。 因此,例如,该程序将显示True ,因为StartNew使用的lambda在ConcurrentExclusiveSchedulerPairExclusiveScheduler实例中执行,而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.CurrentTaskScheduler.Current 。 当您“期望” C#中的某些内容时,编译器将用于轮询(通过调用GetAwaiter )的“预期”(在本例中为Task)的代码转换为“ waiting”( TaskAwaiter )。 “等待”负责附加一个回调(通常称为“继续”),当等待完成时,该回调将返回状态机。 他使用在回调注册期间捕获的上下文/调度程序来实现此目的。 我们将进行一些优化和配置,如下所示:

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

在这里,首先检查是否SynchronizationContextSynchronizationContext ,如果没有SynchronizationContext ,则检查是否TaskScheduler非标准TaskScheduler 。 如果有一个,则当回调准备好进行呼叫时,将使用捕获的调度程序;否则,将使用捕获的调度程序。 如果不是,则回调将作为完成预期任务的操作的一部分执行。

ConfigureAwait的作用(假)


ConfigureAwait方法不是特殊的:编译器或运行时不会以任何特定方式识别它。 这是返回结构的常规方法( ConfiguredTaskAwaitable包装原始任务)并采用布尔值。 请记住,可以将await与实现正确模式的任何类型一起使用。 如果返回另一种类型,则意味着编译器可以访问实例的GetAwaiter方法(模式的一部分),但使用的是ConfigureAwait返回的类型,而不是直接从任务执行。 这使您可以更改此特殊await行为。

等待而不是等待TaskConfigureAwait(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 taskawait 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); //  downloadBtn.Content = text; } 

这将导致不当行为。 这同样适用于依赖于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(); //    SomeCoolSyncCtx }); 

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(); // await'      } finally { SynchronizationContext.SetSynchronizationContext(old); } await t; //  -     


, 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,方法GetResultOnCompleted(可选地提供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没有。这意味着默认情况下SynchronizationContextASP.NET Core应用程序中运行的代码将看不到特殊代码,从而减少了ConfigureAwait(false)对此环境的需求。

但是,这并不意味着永远不会有习惯SynchronizationContextTaskScheduler . - ( , ) , , 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)) { ... } 

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

, , .

Source: https://habr.com/ru/post/zh-CN482354/


All Articles