# ConfigureAwait常见问题解答

时间:2020-01-13 22:56:00 来源:互联网 作者: 神秘的大神 字体:

原文: 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线程上运行“消息循环”,该线程只是等待新消息到达以进行处理。这些消息可能用于鼠标移动和单击,用于键盘键入,用于系统事件,可供可调用的委托等。因此,给定SynchronizationContextWindows 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 TaskTask计划到运行时将不运行ExclusiveScheduler,一次只能Task运行一个互斥对象…这样,它的行为非常类似于读/写锁。

类似SynchronizationContextTaskScheduler还具有一个Current属性,该属性返回“当前的TaskScheduler。不类似于SynchronizationContext,然而,这没有设置当前调度程序的方法。相反,当前调度程序是与当前正在运行Task的调度程序相关联的调度程序,并且调度程序作为启动的一部分提供给系统Task。因此,举例来说,使用这个程序将输出“True”,使用lambda在StartNew上执行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方法,它创建了一个新的TaskSchedulerSynchronizationContext.Current返回的内容上排队运行Post

SynchronizationContext和TaskScheduler与等待如何关联?

考虑在UI应用使用编Button。在点击Button,我们要下载从网站一些文字,并将其设置为ButtonContent。该Button只应该从拥有它的UI线程访问,所以当我们已经成功地下载了新的日期和时间文本和想把它存回ButtonContent,我们需要从拥有控制线程这样做。如果不这样做,则会出现类似以下的异常:

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)在这种环境中运行的需要。

但是,这并不意味着永远不会有习俗SynchronizationContextTaskScheduler礼物。如果某些用户代码(或您的应用程序正在使用的其他库代码)设置了自定义上下文并调用了您的代码,或者按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。除非您显式禁用ExecutionContextExecutionContext.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。

如果这对您很重要,或者您觉得这里有新的有趣的想法,我鼓励您为这些或新的讨论贡献自己的想法。