ToB企服应用市场:ToB评测及商务社交产业平台

标题: .NET ConfigureAwait FAQ (翻译) [打印本页]

作者: 玛卡巴卡的卡巴卡玛    时间: 2024-7-19 12:38
标题: .NET ConfigureAwait FAQ (翻译)
  阅读了 https://devblogs.microsoft.com/dotnet/configureawait-faq/,感觉其对于 .NET 异步编程有非常有意义的指导,对于进一步学习和明白 .NET 异步编程非常友邦做,所以进行翻译以供参考学习。
 
  七年多前,.NET 在语言和库中加入了 async/await 。在这段时间里,它像野火一样燎原,不但在 .NET 生态系统中流行开来,还被无数其他语言和框架所效仿。在 .NET 中,它也得到了大量的改进,包括利用异步的附加语言构造、提供异步支持的 API,并从根本上改进了 async/await 运行的基础架构(特殊是 .NET Core 中的性能和诊断功能改进)。
  不过,async/await 的一个方面仍然存在问题,那就是 ConfigureAwait。在本篇文章中,我盼望能回答此中的许多问题。我盼望这篇文章从头到尾都具有可读性,同时也是一个常见问题列表(FAQ),可供今后参考。
  要真正了解 ConfigureAwait,我们需要从更早的时候开始...
什么是 SynchronizationContext?

  System.Threading.SynchronizationContext 文档指出,它 “为在各种同步模型中传播同步上下文提供了基本功能”。如许的形貌并不明显。
  对于 99.9% 的用例来说,SynchronizationContext 只是一个提供捏造 Post 方法的类型,该方法吸取一个异步执行的委托(SynchronizationContext 上还有其他各种捏造成员,但它们用得很少,与本讨论无关)。基础类型的 Post 字面上只是调用 ThreadPool.QueueUserWorkItem 来异步调用所提供的委托。但是,派生类型会覆盖 Post,以便在最符合的时间、最符合的地点执行委托。
  例如,Windows 窗体有一个 SynchronizationContext 派生类型,该类型重载了 Post,使其与 Control.BeginInvoke 的功能等效;这意味着对其 Post 方法的任何调用都将导致委托在稍后的某个时刻在与相关控件(又称 “UI 线程”)关联的线程上被调用。Windows 窗体依赖于 Win32 消息处理,并在用户界面线程上运行一个 “消息循环”,该线程只需等候新消息的到来即可进行处理。这些消息可能是鼠标移动和点击、键盘输入、系统变乱、可调用的委托等。因此,如果给定了 Windows 窗体应用程序 UI 线程的 SynchronizationContext 实例,要在 UI 线程上执行委托,只需将其通报给 Post 即可。
  Windows Presentation Foundation(WPF)也是如此。它有本身的 SynchronizationContext 派生类型,此中的 Post 重载同样可以将委托 “marshals ”到 UI 线程(通过 Dispatcher.BeginInvoke),在这种情况下,委托是由 WPF Dispatcher 而不是 Windows Forms 控件管理的。
  而对于 Windows RunTime(WinRT)。它有本身的 SynchronizationContext 派生类型,具有 Post 覆盖功能,还能通过其 CoreDispatcher 将委托队列到 UI 线程。
这不但仅是 “在用户界面线程上运行此委托”。任何人都可以实现一个带有 Post 的同步上下文(SynchronizationContext),它可以做任何事情。例如,我可能并不关心委托在哪个线程上运行,但我盼望确保任何 Post 到我的 SynchronizationContext 的委托都能在肯定水平的并发性下执行。我可以使用类似如许的自界说 SynchronizationContext 来实现这一目标:
  1. internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext
  2. {
  3.     private readonly SemaphoreSlim _semaphore;
  4.     public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel) =>
  5.         _semaphore = new SemaphoreSlim(maxConcurrencyLevel);
  6.     public override void Post(SendOrPostCallback d, object state) =>
  7.         _semaphore.WaitAsync().ContinueWith(delegate
  8.         {
  9.             try { d(state); } finally { _semaphore.Release(); }
  10.         }, default, TaskContinuationOptions.None, TaskScheduler.Default);
  11.     public override void Send(SendOrPostCallback d, object state)
  12.     {
  13.         _semaphore.Wait();
  14.         try { d(state); } finally { _semaphore.Release(); }
  15.     }
  16. }
复制代码
 
  事实上,单元测试框架 xunit 提供的 SynchronizationContext(同步上下文)与此非常相似,它用于限制与可并发运行的测试相关的代码量。
  全部这一切的利益与任何抽象的利益都是一样的:它提供了一个单一的 API,可用于对委托进行队列,以便按照实现创建者的意愿进行处理,而无需了解该实现的细节。因此,如果我正在编写一个库,而我想去做一些工作,然后将一个委托队列回原始位置的 “上下文”,我只需要抓取它们的 SynchronizationContext,并将其保留下来,然后当我完成我的工作时,在该上下文上调用 Post 来移交我想调用的委托。我不需要知道,对于 Windows 窗体,我应该抓取一个控件并使用它的 BeginInvoke;或者对于 WPF,我应该抓取一个 Dispatcher 并使用它的 BeginInvoke;或者对于 xunit,我应该以某种方式获取它的上下文并对其进行队列;我只需要抓取当前的 SynchronizationContext 并在稍后使用它。为此,SynchronizationContext 提供了一个 Current 属性,因此要实现上述目标,我可以编写如下代码:
  1. public void DoWork(Action worker, Action completion)
  2. {
  3.     SynchronizationContext sc = SynchronizationContext.Current;
  4.     ThreadPool.QueueUserWorkItem(_ =>
  5.     {
  6.         try { worker(); }
  7.         finally { sc.Post(_ => completion(), null); }
  8.     });
  9. }
复制代码
  盼望从当前环境公开自界说上下文的框架会使用 SynchronizationContext.SetSynchronizationContext 方法。
什么是任务调度器?

  SynchronizationContext 是 “调度程序 ”的一般抽象。个别框架有时会有本身的调度程序抽象,System.Threading.Tasks 也不例外。当任务由委托支持,可以列队和执行时,它们就与 System.Threading.Tasks.TaskScheduler 关联。正如 SynchronizationContext 提供了一个捏造的 Post 方法来对委托的调用进行列队(实现随后通过典型的委托调用机制调用委托),TaskScheduler 也提供了一个抽象的 QueueTask 方法(实现随后通过 ExecuteTask 方法调用该任务)。
  TaskScheduler.Default 返回的默认调度程序是线程池,但也可以派生自 TaskScheduler 并重写相关方法,以实现在何时何地调用任务的任意行为。例如,核心库包括 System.Threading.Tasks.ConcurrentExclusiveSchedulerPair 类型。该类的实例公开了两个 TaskScheduler 属性,一个称为 ExclusiveScheduler,另一个称为 ConcurrentScheduler。调度到 ConcurrentScheduler 的任务可以并发运行,但必须遵守在构建 ConcurrentExclusiveSchedulerPair 时提供给它的限制(类似于前面表现的 MaxConcurrencySynchronizationContext),而且当调度到 ExclusiveScheduler 的任务运行时,ConcurrentScheduler 任务不会运行,每次只允许运行一个独占任务......如许,它的行为就非常像读写锁。
  与 SynchronizationContext 一样,TaskScheduler 也有一个 Current 属性,用于返回 “当前 ”TaskScheduler。但与 SynchronizationContext 不同的是,没有设置当前调度程序的方法。当前调度程序是与当前运行的任务相关联的调度程序,而调度程序是作为启动任务的一部分提供给系统的。因此,举例来说,这个程序将输出 “True”,由于 StartNew 使用的 lambda 是在 ConcurrentExclusiveSchedulerPair 的 ExclusiveScheduler 上执行的,并且会看到 TaskScheduler.Current 被设置为该调度程序:
  1. using System;
  2. using System.Threading.Tasks;
  3. class Program
  4. {
  5.     static void Main()
  6.     {
  7.         var cesp = new ConcurrentExclusiveSchedulerPair();
  8.         Task.Factory.StartNew(() =>
  9.         {
  10.             Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler);
  11.         }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler).Wait();
  12.     }
  13. }
复制代码
  风趣的是,TaskScheduler 提供了一个静态 FromCurrentSynchronizationContext 方法,该方法可创建一个新的 TaskScheduler,使用其 Post 方法对任务进行列队,以便在 SynchronizationContext.Current 返回的任务上列队运行。
SynchronizationContext 和 TaskScheduler 与 await 有什么关系?

  考虑编写一个带有按钮的 UI 应用程序。点击按钮后,我们盼望从一个网站下载一些文本,并将其设置为按钮的内容。按钮只能从拥有它的用户界面线程中访问,因此当我们乐成下载了新的日期和时间文本并想将其存储回按钮的内容时,我们需要从拥有控件的线程中进行操纵。否则就会出现以下异常:
  1. System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'
复制代码
  如果是手动编写,我们可以使用 SynchronizationContext(如前所述)将 “内容 ”的设置传送回原始上下文,例如通过 TaskScheduler(任务调度程序):
  1. private static readonly HttpClient s_httpClient = new HttpClient();
  2. private void downloadBtn_Click(object sender, RoutedEventArgs e)
  3. {
  4.     s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
  5.     {
  6.         downloadBtn.Content = downloadTask.Result;
  7.     }, TaskScheduler.FromCurrentSynchronizationContext());
  8. }
复制代码
  或直接使用 SynchronizationContext:
  1. private static readonly HttpClient s_httpClient = new HttpClient();
  2. private void downloadBtn_Click(object sender, RoutedEventArgs e)
  3. {
  4.     SynchronizationContext sc = SynchronizationContext.Current;
  5.     s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
  6.     {
  7.         sc.Post(delegate
  8.         {
  9.             downloadBtn.Content = downloadTask.Result;
  10.         }, null);
  11.     });
  12. }
复制代码
  不过,这两种方法都明确使用了回调。相反,我们盼望用 async/await 来天然地编写代码:
  1. private static readonly HttpClient s_httpClient = new HttpClient();
  2. private async void downloadBtn_Click(object sender, RoutedEventArgs e)
  3. {
  4.     string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
  5.     downloadBtn.Content = text;
  6. }
复制代码
  这就 “just works”,乐成地在 UI 线程上设置了内容,由于就像上面手动实现的版本一样,等候任务默认会关注 SynchronizationContext.Current 和 TaskScheduler.Current。在 C# 中等候任何任务时,编译器会转换代码以询问(通过调用 GetAwaiter)“awaitable”(此处为任务)“awaiter”(此处为 TaskAwaiter)。该等候者负责连接回调(通常称为 “继承”),当等候对象完成时,回调将回调到状态机,并使用回调注册时捕获的上下文/调度程序来完成。虽然所使用的代码并不完全相同(还进行了额外的优化和调整),但差不多是如许的:
  1. object scheduler = SynchronizationContext.Current;
  2. if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
  3. {
  4.     scheduler = TaskScheduler.Current;
  5. }
复制代码
  换句话说,它会首先检查是否设置了同步上下文(SynchronizationContext),如果没有,则检查是否存在非默认的任务调度程序(TaskScheduler)。如果找到了,当回调预备好被调用时,它就会使用捕获的调度程序;否则,它一般只会在完成等候任务的操纵中执行回调。
ConfigureAwait(false) 的作用是什么?

  ConfigureAwait 方法并不特殊:编译器或运行时都不会以任何特殊方式识别它。它只是一个返回结构体(ConfiguredTaskAwaitable)的方法,该结构体封装了调用它的原始任务以及指定的布尔值。请记住,await 可以用于任何袒露精确模式的类型。通过返回不同的类型,这意味着当编译器访问实例 GetAwaiter 方法(模式的一部分)时,它是根据 ConfigureAwait 返回的类型而不是直接根据任务来访问的,这就提供了一个钩子,可以通过这个自界说的 awaiter 来改变 await 的行为方式。
  详细来说,等候从 ConfigureAwait 返回的类型(continueOnCapturedContext: false)而不是直接等候任务,最终会影响前面所示的如何捕获目标上下文/调度程序的逻辑。这现实上使之前表现的逻辑变得更像如许:
  1. object scheduler = null;
  2. if (continueOnCapturedContext)
  3. {
  4.     scheduler = SynchronizationContext.Current;
  5.     if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
  6.     {
  7.         scheduler = TaskScheduler.Current;
  8.     }
  9. }
复制代码
  换句话说,通过指定 false,即使当前上下文或调度程序可以回调,它也会假装没有。
为什么我要使用 ConfigureAwait(false)?

  ConfigureAwait(continueOnCapturedContext: false) 用于制止在原始上下文或调度程序上逼迫调用回调。如许做有几个利益:
  提高性能:队列回调而不是直接调用回调是有代价的,这一方面是由于会涉及额外的工作(通常是额外的分配),另一方面是由于这意味着我们无法在运行时采用某些优化(当我们确切知道如何调用回调时,我们可以进行更多优化,但如果将回调交给抽象的任意实现,我们有时会受到限制)。对于非常热的路径,即使是检查当前同步上下文(SynchronizationContext)和当前任务调度器(TaskScheduler)(两者都涉及访问线程状态)的额外成本,也会增长可衡量的开销。如果 await 之后的代码现实上不需要在原始上下文中运行,那么使用 ConfigureAwait(false) 就可以制止全部这些开销:它不需要进行不须要的列队,可以利用全部可以利用的优化,还可以制止不须要的线程静态访问。
  制止死锁:考虑一个对网络下载结果使用 await 的库方法。您调用该方法并同步阻塞等候其完成,例如使用 .Wait() 或 .Result 或 .GetAwaiter().GetResult() 来关闭返回的任务对象。现在考虑一下,如果您在当前同步上下文(SynchronizationContext)中调用该方法,而当前同步上下文将其上可运行的操纵数目限制为 1,无论是显式地通过类似前面所示的 MaxConcurrencySynchronizationContext,还是隐式地通过只有一个线程可使用的上下文(如 UI 线程),都会发生什么情况。因此,我们在这一个线程上调用方法,然后阻塞它,等候操纵完成。该操纵启动网络下载并等候下载。默认情况下,等候任务会捕获当前的 SynchronizationContext,因此它会捕获当前的 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 窗体应用程序中编写变乱处理程序、在 xunit 中编写单元测试、在 ASP.NET MVC 控制器中编写代码,无论应用程序模型是否确实发布了 SynchronizationContext,您都盼望在 SynchronizationContext 存在时使用它。这意味着默认情况下/ConfigureAwait(true)。您只需简单地使用 await,回调/连续操纵就会精确地发布回原始上下文(如果存在的话)。由此得出的一般指导原则是:如果编写应用程序级代码,请勿使用 ConfigureAwait(false)。回想一下本文章前面的 Click 变乱处理程序代码示例:
  1. private static readonly HttpClient s_httpClient = new HttpClient();
  2. private async void downloadBtn_Click(object sender, RoutedEventArgs e)
  3. {
  4.     string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
  5.     downloadBtn.Content = text;
  6. }
复制代码
  DownloadBtn.Content = text 的设置需要在原始上下文中完成。如果代码违反了这一准则,在不应该使用 ConfigureAwait(false) 的情况下使用了它:
  1. private static readonly HttpClient s_httpClient = new HttpClient();
  2. private async void downloadBtn_Click(object sender, RoutedEventArgs e)
  3. {
  4.     string text = await s_httpClient.GetStringAsync("http://example.com/currenttime").ConfigureAwait(false); // bug
  5.     downloadBtn.Content = text;
  6. }
复制代码
  将导致不良行为。依赖 HttpContext.Current 的经典 ASP.NET 应用程序中的代码也是如此;使用 ConfigureAwait(false),然后实验使用 HttpContext.Current 很可能会导致问题。
  相比之下,通用库之所以 “通用”,部分原因在于它们不关心使用环境。您可以在网络应用程序、客户端应用程序或测试中使用它们,这并不重要,由于库代码与可能使用的应用程序模型无关。不可知性还意味着它不会做任何需要以特定方式与应用程序模型交互的事情,例如,它不会访问 UI 控件,由于通用库对 UI 控件一无所知。既然我们不需要在任何特定环境中运行代码,我们就可以制止将续程/回调逼迫返回到原始上下文,我们可以通过使用 ConfigureAwait(false)来做到这一点,并获得其带来的性能和可靠性优势。这就引出了一个普遍的指导原则:如果你正在编写通用库代码,请使用 ConfigureAwait(false)。举例来说,这就是为什么您会看到 .NET Core 运行时库中的每一个(或险些每一个)await 都在每一个 await 上使用 ConfigureAwait(false);除了少数例外情况,如果不使用 ConfigureAwait(false),则很可能是需要修复的错误。例如,这个 PR 修复了 HttpClient 中一个缺失的 ConfigureAwait(false) 调用。
  当然,与全部指南一样,也会有例外情况,在某些地方它并不合理。例如,在通用程序库中,一个较大的例外情况(或至少是需要考虑的类别)是,这些程序库的 API 需要委托才气调用。在这种情况下,库的调用者通报的可能是应用程序级的代码,由库来调用,这现实上使库的那些 “通用 ”假设变得毫无意义。例如,考虑 LINQ 的 Where 方法的异步版本,如  public static async IAsyncEnumerable WhereAsync(this IAsyncEnumerable source, Func predicate) 。这里的 predicate 是否需要调用回调用者的原始 SynchronizationContext?这取决于 WhereAsync 的实现,这也是它可能选择不使用 ConfigureAwait(false) 的原因。
  即使有这些特殊情况,总体指导仍然有用,而且是一个很好的出发点:如果你正在编写通用库/应用程序模型无关代码,请使用 ConfigureAwait(false),否则就不要使用。
ConfigureAwait(false) 是否能保证回调不会在原始上下文中运行?

  但这并不意味着在 await task.ConfigureAwait(false) 之后的代码不会在原始上下文中运行。这是由于已完成的 awaitables 上的 await 只是同步运行过 await,而不是逼迫将任何内容列队返回。因此,如果你等候一个在等候时已经完成的任务,无论你是否使用了 ConfigureAwait(false),紧随其后的代码都将继承在当前线程的任何上下文中执行。
只在我的方法中的第一个 await 上使用 ConfigureAwait(false),而不在其他 await 上使用,如许可以吗?

  一般来说,不会。请参见前面的常见问题。如果 await 任务.ConfigureAwait(false)涉及的任务在等候时已经完成(这种情况现实上非常常见),那么 ConfigureAwait(false) 就没有意义了,由于线程会继承在之后的方法中执行代码,并且仍在之前的相同上下文中。
  一个值得留意的例外情况是,如果你知道第一个 await 将始终异步完成,并且被等候的事物将在没有自界说 SynchronizationContext 或 TaskScheduler 的环境中调用其回调。例如,.NET 运行时库中的 CryptoStream 盼望确保其潜在的盘算麋集型代码不会作为调用者同步调用的一部分运行,因此它使用了自界说 awaiter,以确保第一个等候之后的全部内容都在线程池线程上运行。不过,即使在这种情况下,你也会留意到下一个 await 仍然使用了 ConfigureAwait(false);从技术上讲,这并不是必须的,但它让代码查察变得更轻易,由于否则每次查看这段代码时,就不需要分析为什么不使用 ConfigureAwait(false)了。
能否使用 Task.Run 来制止使用 ConfigureAwait(false)?

  是的,如下示例代码:
  1. Task.Run(async delegate
  2. {
  3.     await SomethingAsync(); // won't see the original context
  4. });
复制代码
  在 SomethingAsync() 上调用 ConfigureAwait(false) 将是无效的,由于通报给 Task.Run 的委托将在线程池线程上执行,堆栈上没有更高的用户代码,因此 SynchronizationContext.Current 将返回空值。此外,Task.Run 还隐式地使用了 TaskScheduler.Default,这意味着在委托中查询 TaskScheduler.Current 也将返回 Default。这意味着无论是否使用了 ConfigureAwait(false),await 都将表现出相同的行为。此外,它也不保证该 lambda 内部的代码会做什么。如下代码:
  1. Task.Run(async delegate
  2. {
  3.     SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx());
  4.     await SomethingAsync(); // will target SomeCoolSyncCtx
  5. });
复制代码
  SomethingAsync 中的代码现实上就会将 SynchronizationContext.Current 视为 SomeCoolSyncCtx 实例,并且该等候和 SomethingAsync 中任何未设置的等候都会返回到该实例。因此,要使用这种方法,你需要了解你正在列队的全部代码可能会做什么,也可能不会做什么,它的操纵是否会妨碍你的操纵。
  这种方法的代价是需要创建/队列一个额外的任务对象。这对您的应用程序或库来说可能重要,也可能不重要,这取决于您对性能的敏感度。
  此外,请记住,这些技巧可能会带来更多问题,并产生其他意想不到的后果。例如,有人编写了静态分析工具(如 Roslyn 分析器)来标记未使用 ConfigureAwait(false) 的等候,如 CA2007。如果你启用了如许的分析器,但又为了制止使用 ConfigureAwait 而使用了如许的技巧,那么分析器很有可能会标记它,从而给你带来更多的工作。因此,也许你会由于分析器的嘈杂而禁用它,而现在你最终会遗漏代码库中其他本应使用 ConfigureAwait(false) 的地方。
我能否使用 SynchronizationContext.SetSynchronizationContext 来制止使用 ConfigureAwait(false)?

  不,也许吧。这取决于所涉及的代码。
  有些开发人员是如许编写代码的:
  1. Task t;
  2. SynchronizationContext old = SynchronizationContext.Current;
  3. SynchronizationContext.SetSynchronizationContext(null);
  4. try
  5. {
  6.     t = CallCodeThatUsesAwaitAsync(); // awaits in here won't see the original context
  7. }
  8. finally { SynchronizationContext.SetSynchronizationContext(old); }
  9. await t; // will still target the original context
复制代码
  盼望它能让 CallCodeThatUsesAwaitAsync 中的代码将当前上下文视为空。确实如此。因此,如果这段代码运行在某个自界说的 TaskScheduler 上,CallCodeThatUsesAwaitAsync 中的等候(且未使用 ConfigureAwait(false))仍将看到并队列回该自界说 TaskScheduler。
  全部留意事项与之前的 Task.Run 相关常见问题解答中的一样:这种变通方法会产生影响,而且 try 内的代码也可以通过设置不同的上下文(或调用非默认 TaskScheduler 的代码)来挫败这些实验。
  对于这种模式,您还需要留意一个渺小的变革:
  1. SynchronizationContext old = SynchronizationContext.Current;
  2. SynchronizationContext.SetSynchronizationContext(null);
  3. try
  4. {
  5.     await t;
  6. }
  7. finally { SynchronizationContext.SetSynchronizationContext(old); }
复制代码
  看到问题所在了吗?这有点丢脸,但也有可能造成很大影响。我们无法保证 await 最终会在原始线程上调用回调/继承,这意味着将 SynchronizationContext 重置回原始线程可能不会真正发生在原始线程上,这可能会导致该线程上的后续工作项看到错误的上下文(为了解决这个问题,编写良好的应用程序模型在设置自界说上下文时通常会添加代码,以便在调用任何进一步的用户代码前手动重置上下文)。即使它碰巧运行在同一线程上,也可能要过一段时间才气运行,因此上下文在一段时间内不会得到适当规复。如果运行在不同的线程上,最终可能会将错误的上下文设置到该线程上。诸如此类,不一而足。这与抱负状态相去甚远。
我正在使用 GetAwaiter().GetResult(),我需要使用 ConfigureAwait(false) 吗?

  ConfigureAwait 只影响回调。详细来说,awaiter 模式要求 awaiter 公开 IsCompleted 属性、GetResult 方法和 OnCompleted 方法(可选择 UnsafeOnCompleted 方法)。ConfigureAwait 只影响 {Unsafe}OnCompleted 的行为,因此如果你只是直接调用 awaiter 的 GetResult() 方法,那么无论是在 TaskAwaiter 还是在 ConfiguredTaskAwaitable.ConfiguredTaskAwaiter 上进行调用,行为上都不会有任何区别。因此,如果您在代码中看到 task.ConfigureAwait(false).GetAwaiter().GetResult(),您可以将其替换为 task.GetAwaiter().GetResult()(同时也要考虑您是否真的想如许阻塞)。
我知道我运行的环境永远不会有自界说同步上下文或自界说任务调度程序,我可以不使用 ConfigureAwait(false)吗?

  也许吧,这取决于你对 “从不 ”这部分有多大把握。正如之前的常见问题中提到的,您正在使用的应用程序模型没有设置自界说同步上下文,也没有在自界说任务调度程序上调用您的代码,但这并不意味着其他用户或库代码不会如许做。因此,您需要确保情况并非如此,或者至少认识到可能存在的风险。
我听说在 .NET Core 中不再需要 ConfigureAwait(false),是真的吗?

  错。在 .NET Core 上运行时需要它,原因与在 .NET Framework 上运行时完全相同。这方面没有任何变革。
  不过,变革的是某些环境是否发布了本身的 SynchronizationContext。特殊是,.NET Framework 上的经典 ASP.NET 有本身的 SynchronizationContext,而 ASP.NET Core 则没有。这意味着在 ASP.NET Core 应用程序中运行的代码默认不会看到自界说的 SynchronizationContext,从而淘汰了在这种环境中运行 ConfigureAwait(false) 的需要。
  但这并不意味着永远不会出现自界说 SynchronizationContext 或 TaskScheduler。如果某些用户代码(或您的应用程序使用的其他库代码)设置了自界说上下文并调用了您的代码,或者在调度到自界说 TaskScheduler 的任务中调用了您的代码,那么即使在 ASP.NET Core 中,您的等候也可能会看到非默认上下文或调度器,从而导致您想要使用 ConfigureAwait(false)。当然,在这种情况下,如果您制止同步阻塞(在 Web 应用程序中无论如何都应制止如许做),如果您不介意在这种有限情况下的少量性能开销,您可能可以不使用 ConfigureAwait(false)。
在对 IAsyncEnumerable 进行 “await foreach ”时,能否使用 ConfigureAwait?

  是的,请参阅 MSDN Magazine 这篇文章中的示例。
  await foreach 与一种模式绑定,因此,虽然它可以用于枚举 IAsyncEnumerable,但也可以用于枚举袒露精确 API 表面地区的东西。.NET运行时库包罗一个关于 IAsyncEnumerable 的 ConfigureAwait 扩展方法,该方法返回一个封装了 IAsyncEnumerable 和布尔值的自界说类型,并公开了精确的模式。当编译器生成对枚举器的 MoveNextAsync 和 DisposeAsync 方法的调用时,这些调用会指向返回的已设置枚举器结构类型,并以所需的设置方式执行等候。
在 “等候使用” IAsyncDisposable 时,能否使用 ConfigureAwait?

  是的,不过有一个小麻烦。
  与上一个常见问题中形貌的 IAsyncEnumerable 一样,.NET 运行时库在 IAsyncDisposable 上公开了一个 ConfigureAwait 扩展方法,await 使用者只要实现了适当的模式(即公开了一个适当的 DisposeAsync 方法),就会很高兴地使用它:
  1. await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false))
  2. {
  3.     ...
  4. }
复制代码
  这里的问题是,c 的类型现在不是 MyAsyncDisposableClass,而是 System.Runtime.CompilerServices.ConfiguredAsyncDisposable,也就是从 IAsyncDisposable 上的 ConfigureAwait 扩展方法返回的类型。
  要解决这个问题,你需要多写一行:
  1. var c = new MyAsyncDisposableClass();
  2. await using (c.ConfigureAwait(false))
  3. {
  4.     ...
  5. }
复制代码
  现在,c 的类型又变成了所需的 MyAsyncDisposableClass。这也会增长 c 的作用域;如果这有影响,可以用大括号将整个代码包起来。
我使用了 ConfigureAwait(false),但我的 AsyncLocal 仍在 await 之后流向代码,这是一个错误吗?

  不,这是预料之中的。AsyncLocal 数据作为 ExecutionContext 的一部分流动,而 ExecutionContext 与 SynchronizationContext 是分开的。除非您使用 ExecutionContext.SuppressFlow() 显式禁用了 ExecutionContext 流量,否则 ExecutionContext(以及 AsyncLocal 数据)将始终在等候中流动,无论是否使用了 ConfigureAwait 来制止捕获原始的 SynchronizationContext。更多信息,请参阅本博文
.NET(C#)能否帮助我制止在库中明确使用 ConfigureAwait(false)?

  库开发人员有时会对需要使用 ConfigureAwait(false) 表现不满,并要求提供侵入性较小的替代方法。
  目前还没有任何替代方案,至少没有内置在语言/编译器/运行时中。不过,关于这种解决方案的建议有很多,例如:
  如果这对您很重要,或者您觉得本身有新的风趣的想法,我鼓励您在这些讨论或新的讨论中发表您的想法。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4