ConfigureAwait in .NET8

这篇具有很好参考价值的文章主要介绍了ConfigureAwait in .NET8。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

ConfigureAwait in .NET8

ConfigureAwait(true) 和 ConfigureAwait(false)

首先,让我们回顾一下原版 ConfigureAwait 的语义和历史,它采用了一个名为 continueOnCapturedContext 的布尔参数。

当对任务(TaskTask<T>ValueTaskValueTask<T>)执行 await 操作时,其默认行为是捕获“上下文”的;稍后,当任务完成时,该 async 方法将在该上下文中继续执行。“上下文”是 SynchronizationContext.CurrentTaskScheduler.Current(如果未提供上下文,则回退到线程池上下文)。通过使用 ConfigureAwait(continueOnCapturedContext: true) 可以明确这种在捕获上下文中继续的默认行为。

如果不想在该上下文上恢复,ConfigureAwait(continueOnCapturedContext: false) 就很有用。使用 ConfigureAwait(false) 时,异步方法会在任何可用的线程池线程上恢复。

ConfigureAwait(false) 的历史很有趣(至少对我来说是这样)。最初,社区建议在所有可能的地方使用 ConfigureAwait(false),除非需要上下文。这也是我在 Async 最佳实践一文中推荐的立场。在那段时间里,我们就默认为 true 的原因进行了多次讨论,尤其是那些不得不经常使用 ConfigureAwait(false) 的库开发人员。

不过,多年来,”尽可能使用 ConfigureAwait(false)“的建议已被修改。第一次(尽管是微小的)变化是,不再是”尽可能使用 ConfigureAwait(false)“,而是出现了更简单的指导原则:在库代码中使用 ConfigureAwait(false),而不要在应用代码中使用。这条准则更容易理解和遵循。尽管如此,关于必须使用 ConfigureAwait(false) 的抱怨仍在继续,并不时有人要求在整个项目范围内更改默认值。出于语言一致性的考虑,C# 团队总是拒绝这些请求。

最近(具体来说,自从 ASP.NET 在 ASP.NET Core 中放弃了 SynchronizationContext 并修复了所有需要 sync-over-async(即同步套异步代码) 的地方之后),C# 团队开始放弃使用 ConfigureAwait(false)。作为一名库作者,我完全理解让 ConfigureAwait(false) 在代码库中随处可见是多么令人讨厌!有些库作者决定不再使用 ConfigureAwait(false)。就我自己而言,我仍然在我的库中使用 ConfigureAwait(false),但我理解这种挫败感。

既然谈到了 ConfigureAwait(false),我想指出几个常见的误解:

  1. ConfigureAwait(false) 并不是避免死锁的好方法。这不是它的目的,充其量只是一个值得商榷的解决方案。为了在直接阻塞时避免死锁,你必须确保所有异步代码都使用 ConfigureAwait(false),包括库和运行时中的代码。这并不是一个非常容易维护的解决方案。还有更好的解决方案。
  2. ConfigureAwait 配置的是 await,而不是任务。例如,SomethingAsync().ConfigureAwait(false).GetAwaiter().GetResult() 中的 ConfigureAwait(false) 完全没有任何作用。同样,var task = SomethingAsync(); task.ConfigureAwait(false); await task; 中的 await 仍在捕获的上下文中继续,完全忽略了 ConfigureAwait(false)。多年来,我见过这两种错误。
  3. ConfigureAwait(false) 并不意味着”在线程池线程上运行此方法的后续部分“或”在不同的线程上运行此方法的后续部分“。它只在 await 暂停执行并稍后恢复异步方法时生效。具体来说,如果 await 的任务已经完成,它将不会暂停执行;在这种情况下,ConfigureAwait 将不会起作用,因为await 会同步继续执行。

好了,既然我们已经重新理解了 ConfigureAwait(false),下面就让我们看看 ConfigureAwait 在 .NET8 中是如何得到增强的。ConfigureAwait(true)ConfigureAwait(false) 仍具有相同的行为。但是,有一种新的 ConfigureAwait 即将出现!

ConfigureAwait(ConfigureAwaitOptions)

ConfigureAwait 有几个新选项。ConfigureAwaitOptions 是一种新类型,它提供了配置 awaitables 的所有不同方法:

namespace System.Threading.Tasks;
[Flags]
public enum ConfigureAwaitOptions
{
    None = 0x0,
    ContinueOnCapturedContext = 0x1,
    SuppressThrowing = 0x2,
    ForceYielding = 0x4,
}

首先,请注意:这是一个 Flags 枚举;这些选项的任何组合都可以一起使用。

接下来我要指出的是,至少在 .NET8 中,ConfigureAwait(ConfigureAwaitOptions) 仅适用于 TaskTask<T>。它还没有添加到 ValueTask/ValueTask<T>。未来的 .NET 版本有可能为 ValueTask 添加 ConfigureAwait(ConfigureAwaitOptions),但目前它仅适用于引用任务,因此如果您想在 ValueTask 中使用这些新选项,则需要调用 AsTask

现在,让我们依次讲解这些选项。

ConfigureAwaitOptions.None 和 ConfigureAwaitOptions.ContinueOnCapturedContext

这两个选项都很熟悉,但有一点不同。

ConfigureAwaitOptions.ContinueOnCapturedContext--从名字就能猜到与 ConfigureAwait(continueOnCapturedContext: true) 相同。换句话说,await 将捕获上下文,并在该上下文上继续执行异步方法。

Task task = ...;

// 下面做的事情相同
await task;
await task.ConfigureAwait(continueOnCapturedContext: true);
await task.ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext);

ConfigureAwaitOptions.NoneConfigureAwait(continueOnCapturedContext: false) 相同。换句话说,除了不捕获上下文外,await 的行为完全正常;假设 await 确实产生了结果(即任务尚未完成),那么异步方法将在任何可用的线程池线程上继续执行。

Task task = ...;

// 下面两行代码效果一样
await task.ConfigureAwait(continueOnCapturedContext: false);
await task.ConfigureAwait(ConfigureAwaitOptions.None);

这里有一个转折点:使用新选项后,默认情况下不会捕获上下文!除非你在标记中明确包含 ContinueOnCapturedContext,否则上下文将不会被捕获。当然,await 本身的默认行为不会改变:在没有任何 ConfigureAwait 的情况下,await 的行为将与使用了 ConfigureAwait(true)ConfigureAwaitOptions.ContinueOnCapturedContext) 时一样。

Task task = ...;

// 默认的行为还是会继续捕捉上下文
await task;

// 默认选项 (ConfigureAwaitOptions.None): 不会捕捉上下文
await task.ConfigureAwait(ConfigureAwaitOptions.None);

因此,在开始使用这个新的 ConfigureAwaitOptions 枚举时,请记住这一点。

ConfigureAwaitOptions.SuppressThrowing

SuppressThrowing 标志可抑制等待任务时可能出现的异常。在正常情况下,await 会通过在 await 时重新引发异常来观察任务异常。通常情况下,这正是你想要的行为,但在某些情况下,你只想等待任务完成,而不在乎任务是成功完成还是出现异常。那么 SuppressThrowing 选项允许您等待任务完成,而不观察其结果。

Task task = ...;

// 下面两行代码等价
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
try { await task.ConfigureAwait(false); } catch { }

我预计这将与取消任务一起发挥最大作用。在某些情况下,有些代码需要先取消任务,然后等待现有任务完成后再启动替代任务。在这种情况下,SuppressThrowing 将非常有用:代码可以使用 SuppressThrowing 等待,当任务完成时,无论任务是成功、取消还是出现异常,方法都将继续。

// 取消旧任务并等待完成,忽略异常情况
_cts.Cancel();
await _task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

// 开启新任务
_cts = new CancellationTokenSource();
_task = SomethingAsync(_cts.Token);

如果使用 SuppressThrowing 标志等待,异常就会被视为”已观察到“,因此不会引发 TaskScheduler.UnobservedTaskException 异常。我们的假设是,你在等待任务时故意丢弃了异常,所以它不会被认为是未观察到的。

TaskScheduler.UnobservedTaskException += (_, __) => { Console.WriteLine("never printed"); };

Task task = Task.FromException(new InvalidOperationException());
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
task = null;

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Console.ReadKey();

这个标记还有另一个考虑因素。当与 Task 一起使用时,其语义很清楚:如果任务失败了,异常将被忽略。但是,同样的语义对 Task<T> 并不完全适用,因为在这种情况下,await 表达式需要返回一个值(T 类型)。目前还不清楚在忽略异常的情况下返回 T 的哪个值合适,因此当前的行为是在运行时抛出 ArgumentOutOfRangeException。为了帮助在编译时捕捉到这种情况,最近添加了一个新的警告:CA2261 ConfigureAwaitOptions.SuppressThrowing 仅支持非泛型任务。该规则默认为警告,但我建议将其设为错误,因为它在运行时总是会失败。

Task<int> task = Task.FromResult(13);

// 在构建时导致 CA2261 警告,在运行时导致 ArgumentOutOfRangeException。
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

最后要说明的是,除了 await 之外,该标记还影响同步阻塞。具体来说,您可以调用 .GetAwaiter().GetResult() 来阻塞从 ConfigureAwait 返回的 awaiter。无论使用 await 还是 GetAwaiter().GetResult()SuppressThrowing 标记都会导致异常被忽略。以前,当 ConfigureAwait 只接受一个布尔参数时,你可以说”ConfigureAwait 配置了 await“;但现在你必须说得更具体:”ConfigureAwait 返回了一个已配置的 await“。现在,除了 await 的行为外,配置的 awaitable 还有可能修改阻塞代码的行为。除了修改 await 的行为之外。现在的 ConfigureAwait 可能有点误导性,但它仍然主要用于配置 await。当然,不推荐在异步代码中进行阻塞操作。

Task task = Task.Run(() => throw new InvalidOperationException());

// 同步阻塞任务(不推荐)。不会抛出异常。
task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing).GetAwaiter().GetResult();

ConfigureAwaitOptions.ForceYielding

最后一个标志是 ForceYielding 标志。我估计这个标志很少会用到,但当你需要它时,你就需要它!

ForceYielding 类似于 Task.YieldYield 返回一个特殊的 awaitable,它总是声称尚未完成,但会立即安排其继续。这意味着 await 始终以异步方式执行,让出给调用者,然后异步方法尽快继续执行。await 的正常行为是检查可等待对象是否完成,如果完成,则继续同步执行;ForceYielding 阻止了这种同步行为,强制 await 以异步方式执行。

就我个人而言,我发现强制异步行为在单元测试中最有用。在某些情况下,它还可以用来避免堆栈潜入。在实现异步协调基元(如我的 AsyncEx 库中的原语)时,它也可能很有用。基本上,在任何需要强制 await 以异步方式运行的地方,都可以使用 ForceYielding 来实现。

我觉得有趣的一点是,使用 ForceYieldingawait 会让 await 的行为与 JavaScript 中的一样。在 JavaScript 中,await 总是会产生结果,即使你传递给它一个已解析的 Promise 也是如此。在 C# 中,您现在可以使用 ForceYielding 来等待一个已完成的任务,await 的行为就好像它尚未完成一样,就像 JavaScript 的 await 一样。

static async Task Main()
{
  Console.WriteLine(Environment.CurrentManagedThreadId); // main thread
  await Task.CompletedTask;
  Console.WriteLine(Environment.CurrentManagedThreadId); // main thread
  await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
  Console.WriteLine(Environment.CurrentManagedThreadId); // thread pool thread
}

请注意,ForceYielding 本身也意味着不在捕获的上下文中继续执行,因此等同于说”将该方法的剩余部分调度到线程池“或者”切换到线程池线程“。

// ForceYielding 强制 await 以异步方式执行。
// 缺少 ContinueOnCapturedContext 意味着该方法将在线程池线程上继续执行。
// 因此,该语句之后的代码将始终在线程池线程上运行。
await task.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);

Task.Yield 将在捕获的上下文中恢复执行,因此它与仅使用 ForceYielding 不完全相同。实际上,它类似于带有 ContinueOnCapturedContextForceYielding

// 下面两行代码效果相同
await Task.Yield();
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding | ConfigureAwaitOptions.ContinueOnCapturedContext);

当然,ForceYielding 的真正价值在于它可以应用于任何任务。以前,在需要进行让步的情况下,您必须要么添加单独的 await Task.Yield() 语句,要么创建自定义的可等待对象。现在有了可以应用于任何任务的 ForceYielding,这些操作就不再必要了。

拓展阅读

很高兴看到 .NET 团队在多年后仍然在改进 async/await 的功能!

如果您对 ConfigureAwaitOptions 背后的历史和设计讨论更感兴趣,可以查看相关的 Pull Request。在发布之前,曾经有一个名为ForceAsynchronousContinuation 的选项,但后来被删除了。它具有更加复杂的用例,基本上可以覆盖 await 的默认行为,将异步方法的继续操作调度为 ExecuteSynchronously。也许未来的更新会重新添加这个选项,或者也许将来的更新会为 ValueTask 添加 ConfigureAwaitOptions 的支持。我们只能拭目以待!

原文链接

ConfigureAwait in .NET 8 (stephencleary.com)文章来源地址https://www.toymoban.com/news/detail-747244.html

到了这里,关于ConfigureAwait in .NET8的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • .NET8依赖注入新特性Keyed services

    什么是Keyed service Keyed service是指,为一个需要注入的服务定义一个Key Name,并使用使用Key Name检索依赖项注入 (DI) 服务的机制。 使用方法 通过调用 AddKeyedSingleton (或 AddKeyedScoped 或 AddKeyedTransient)来注册服务,与Key Name相关联。或使用 [FromKeyedServices] 属性指定密钥来访问已注册

    2024年02月05日
    浏览(53)
  • .NET8 和 Vue.js 的前后端分离

    在.NET 8中实现前后端分离主要涉及到两个部分:后端API的开发和前端应用的开发。后端API通常使用ASP.NET Core来构建,而前端应用则可以使用任何前端框架或技术栈,比如Vue.js、React或Angular等。下面是一个简化的步骤指南,帮助你在.NET 8中实现前后端分离。 下面是一个简单的示

    2024年04月10日
    浏览(43)
  • .Net8顶级技术:边界检查之IR解析(二)

    IR技术应用在各个编程语言当中,它属于JIT的核心部分,确实有点点麻烦。但部分基本明了。本篇通过.Net8里面的边界检查的小例子了解下。前情提要,看这一篇之前建议看看前一篇:点击此处,以便于理解。 1.前奏 先上C#代码: Test函数经过Roslyn编译成IL代码之后,会被JIT导

    2024年02月06日
    浏览(43)
  • .NET8 Blazor的Auto渲染模式的初体验

    .NET8发布后,Blazor支持四种渲染方式 静态渲染,这种页面只可显示,不提供交互,可用于网页内容展示 使用Blazor Server托管的通过Server交互方式 使用WebAssembly托管的在浏览器端交互方式 使用Auto自动交互方式,最初使用 Blazor Server,并在随后访问时使用 WebAssembly 自动进行交互式

    2024年02月05日
    浏览(50)
  • NET8 ORM 使用AOT SqlSugar 和 EF Core

    .Net8的本地预编机器码NET AOT,它几乎进行了100%的自举。微软为了摆脱C++的钳制,做了很多努力。也就是代码几乎是用C#重写,包括了虚拟机,GC,内存模型等等。而需要C++做的,也就仅仅是引导程序,本篇通过代码来看下这段至关重要的引导程序的运作模式。      SqlSugar已经

    2024年02月05日
    浏览(52)
  • U8二次开发CO-基于Net8调用COM对象

    以前没有碰过U8,只知道基于Net平台构建,本次业务需求是要把钉钉和U8打通,完成代办和消息提醒。网上搜索U8相关二开资料后发现,都是一些技术片段,零零碎碎的不成体系,也有可能是大客户都去U9或者Cloud了,老旧的8面临过气与替换(个人意见),遂边琢磨边做一些示

    2024年04月14日
    浏览(29)
  • Blazor OIDC 单点登录授权实例5 - 独立SSR App (net8 webapp ) 端授权

    目录: OpenID 与 OAuth2 基础知识 Blazor wasm Google 登录 Blazor wasm Gitee 码云登录 Blazor OIDC 单点登录授权实例1-建立和配置IDS身份验证服务 Blazor OIDC 单点登录授权实例2-登录信息组件wasm Blazor OIDC 单点登录授权实例3-服务端管理组件 Blazor OIDC 单点登录授权实例4 - 部署服务端/独立WASM端授

    2024年02月19日
    浏览(43)
  • 开源.NET8.0小项目伪微服务框架(分布式、EFCore、Redis、RabbitMQ、Mysql等)

    为什么说是伪微服务框架,常见微服务框架可能还包括服务容错、服务间的通信、服务追踪和监控、服务注册和发现等等,而我这里为了在使用中的更简单,将很多东西进行了简化或者省略了。 年前到现在在开发一个新的小项目,刚好项目最初的很多功能是比较通用的,所以

    2024年03月09日
    浏览(53)
  • [C#] .NET8增加了Arm架构的多寄存器的查表函数(VectorTableLookup/VectorTableLookupExtension)

    作者: zyl910 发现.NET8增加了Arm架构的多寄存器的查表函数(VectorTableLookup/VectorTableLookupExtension),这给编写SIMD向量化算法带来了方便。 在学习Arm的AdvSimd(Neon)指令集时,发现它的Lookup(查表)功能,类似X86的Sse系列指令集中的字节Shuffle(换位。如 _mm_shuffle_epi8 )功能。 而

    2024年03月24日
    浏览(40)
  • .net8系列-04图文并茂手把手教你配置Swagger支持token以及实现Swagger扩展,Swagger代码单独抽离

    接上篇文章,我们当前已完成如下内容: 创建应用成功 创建接口成功 配置Swagger实现接口注释和版本控制 本文章主要内容为: 配置Swagger支持token传值测试接口 添加如下代码 文件目录:xiaojinWebApplicationxiaojinWebApplicationProgram.cs 打开接口测试页面 配置Token 观察页面 我们发现

    2024年04月27日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包