C# 同步优于异步实现

Fat*_*eni 8 .net c# asp.net

我相信我们都会同意同步优于异步并不是一个好主意。TAP 模型极大地简化了使用 async/await 的异步调用,这就是正确的方法。

不幸的是,对于那些坚持使用旧技术的人来说,这不是一个选择。例如,.NET Framework 中的 Web 服务 (asmx) 不支持 TAP。在大型遗留解决方案中,维护重复的调用链(同步和异步)可能非常令人生畏和烦人。此外,即使您不知道,您也可能正在使用“同步而非异步”方法。许多提供同步 API 的库本质上在内部只是其异步 API 的包装器。例如 Rebus、RestSharp 等。

这是这篇文章/问题的主要目的。尽管我对 async/await 有相当的了解,但我不会假装我知道所有的极端情况(而且有很多)。所以,我想知道专家们对这个话题的看法。这种实施方式可能存在哪些缺点?

https://github.com/restsharp/RestSharp/blob/dev/src/RestSharp/AsyncHelpers.cs

与许多“同步优于异步”实现不同,它们通常只是启动一个新线程以避免可能的死锁Task.Run(async () => {await ...}).Result;此实现使用自定义同步上下文来避免死锁,同时保持在同一线程上。这提供了甚至访问的能力HttpContext.Current(对于 NETFX 应用程序)。

我已经在各种场景中尝试过这个助手,并且效果相当好。但是,我想进一步了解可能出错的地方。

那么,这有多错误呢?有哪些陷阱?

Ste*_*ary 5

不幸的是,对于那些坚持使用旧技术的人来说,这不是一个选择。

我确实有一篇文章讨论了异步同步的几种可能方法,并对每种方法的缺点进行了简短讨论。

例如,.NET Framework 中的 Web 服务 (asmx) 不支持 TAP。

不,但 ASMX 确实支持 APM,您可以编写一个简短的互操作层来将核心 TAP 逻辑公开给 ASMX 并始终保持异步。有一些关于如何做到这一点的示例以及一些TAP-to-APM 帮助程序,它们使互操作层非常干净。

需要同步异步的场景很少;当转换代码不是业务优先事项时,保留一些真正应该异步的代码同步通常是一个务实的决定。

在大型遗留解决方案中,维护重复的调用链(同步和异步)可能非常令人生畏和烦人。

100%同意。

我首选的解决方案是Brownfield Async 文章中“布尔参数 hack”的更现代版本,该解决方案最初是由 Stephen Toub 在对该文章进行技术审查时向我展示的。这是一种使代码保持异步或同步的技术,但不需要任何重复的逻辑。

最近,Stephen Toub 在他关于 .NET 7 性能改进的文章中展示了更现代版本的“布尔参数黑客” (这是一篇很大的文章;搜索“与读写性能相关的一个最终更改”以找到相关的部分)。我拿出了那个宝石并写了一篇关于它的更具体的博客文章。该代码乍一看很奇怪,但它是一种非常强大的技术,这就是我向所有现代库推荐的技术。

这种实施方式可能存在哪些缺点?

此实现安装一个SynchronizationContext包含工作队列的自定义,对初始工作项进行排队,然后同步等待其工作队列完成。它与AsyncContext我的 AsyncEx 库类似。我相信该实现最初源自这个旧的论坛帖子,我在几个地方看到过它的复制,通常带有诸如“我不知道这是如何工作的”之类的评论,说实话这对我来说有点可怕。我从 SO 和其他地方获取代码,但只有在完全理解它之后。

您可以说它是Brownfield Async 文章中的“嵌套消息循环黑客”的变体。AsyncHelpers.RunSync控制当前线程并将其转变为消息泵,处理其自己的工作队列。它安装自己的SynchronizationContext来捕获async延续(默认情况下在捕获上恢复SynchronizationContextTaskScheduler如我在我的博客中描述的那样)。

因此,您将遇到的极端情况都与交换有关SynchronizatonContext。可能无法提供详尽的清单,但我脑海中浮现出一些担忧:

  1. 有些组件需要特定的SynchronizationContext. 一个例子是在 Core ASP.NET出现之前,如果SynchronizationContext.Current不是AspNetSynchronizationContext. 我真的不知道他们为什么这样做,但这是我多年前尝试这种黑客攻击时观察到的行为。另一个例子,一些 UI 组件将验证它们位于正确的同步上下文中(其他组件则验证它们位于正确的线程上)上,如果 SyncCtx 被交换,该线程仍然可以正常工作)。
  2. 有些组件捕获aSynchronizationContext供以后使用,但SynchronizationContext这里使用的寿命有限;一旦任务完成,整个 SyncCtx 就会被拆除。因此,Progress<T>在 SyncCtx 被拆除后,不得使用任何类似于 SyncCtx 的 Rx 可观察观察的东西。
  3. 该解决方案安装一个单线程上下文,然后同步阻塞它。因此,它解决了一类异步同步问题,但如果它调用的任何内容执行阻塞式异步同步,那么它肯定会死锁。
  4. 最后一个是我最关心的问题之一,但也是最难解释的。在我的文章中,我将其称为“意外重入”。如果在 UI 线程(或更具体地说,STA 线程)上运行,这种方法可能会产生令人惊讶的结果。CBrumme 有一些很棒的博客文章,介绍 .NET 运行时如何在阻塞时进行一些STA 泵送;几年前,当 MS 更改其博客 URI 方案时,这些帖子不幸被删除。本质上,这意味着某些 Windows 消息可能会由 UI 线程处理,即使从托管角度来看它是“阻塞的”;这可能会导致部分代码以您不期望的方式运行(在本例中,作为RunSync窗口主消息处理循环的一部分运行)。现在,这些帖子已被删除,现代 .NET CoreAutoResetEvent.WaitOne最终位于WaitForMultipleObjectsIgnoringSyncContext,从名称上看,它可能没有部分发挥作用,所以也许这不再是事实了。但对于我自己来说,我会非常谨慎地在 STA/GUI 线程上做这样的事情。

总而言之,这不是我推荐的方法。我建议改用通用值类型约束接口方法。但是,如果您对将同步运行的所有代码有深入的了解,并且确定不会遇到上述情况,那么这是可以接受的。