首页 > Python基础教程 >
-
# ConfigureAwait常见问题解答
原文: 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)