c# - 在库中编写同步和异步方法并保持 DRY 的模式

标签 c# .net asynchronous task-parallel-library async-await

这个问题在这里已经有了答案:





Using async await when implementing a library with both synchronous and asynchronous API for the same functionality

(2 个回答)


6年前关闭。




我正在修改一个库以添加异步方法。来自 Should I expose synchronous wrappers for asynchronous methods?它声明我不应该只是在 Task.Result 周围写一个包装器调用同步方法时。但是我如何不必在异步方法和同步方法之间复制大量代码,因为我们希望在库中保留这两个选项?

例如图书馆目前使用 TextReader.Read方法。我们希望使用的异步更改的一部分 TextReader.ReadAsync方法。由于这是库的核心,因此我似乎需要在同步和异步方法之间复制大量代码(希望尽可能保持代码 DRY)。或者我需要在 PreRead 中重构它们和 PostRead似乎使代码困惑的方法以及 TPL 试图修复的内容。

我正在考虑包装 TextReader.Read Task.Return() 中的方法.即使它是一项任务,TPL 的改进也不应该让它切换到不同的线程,我仍然可以像往常一样对大部分代码使用 async await。那么是否可以将同步的包装器设为 Task.ResultWait() ?

我查看了 .net 库中的其他示例。 StreamReader似乎在异步和非异步之间复制代码。 MemoryStreamTask.FromResult .

还计划我可以添加的所有地方 ConfigureAwait(false)因为它只是一个图书馆。

更新:

我所说的重复代码是

 public decimal ReadDecimal()
 {
     do
     {
          if (!Read())
          {
               SetInternalProperies()
          }
          else
          {
               return _reader.AsDecimal();
          }
      } while (_reader.hasValue)
 }

 public async Task<decimal> ReadDecimalAsync()
 {
     do
     {
          if (!await ReadAsync())
          {
               SetInternalProperies()
          }
          else
          {
               return _reader.AsDecimal();
          }
      } while (_reader.hasValue)
  }

这是一个小示例,但您可以看到唯一的代码更改是等待和任务。

为了清楚起见,我想在库中的任何地方使用 async/await 和 TPL 进行编码,但我仍然需要让旧的同步方法也能正常工作。我不会只是 Task.FromResult()同步方法。我在想的是有一个标志说我想要同步方法,并在根检查标志类似
 public decimal ReadDecimal()
 { 
     return ReadDecimalAsyncInternal(true).Result;
 }

 public async Task<decimal> ReadDecimal()
 {
     return await ReadDecimalAsyncInternal(false);
 }

 private async Task<decimal> ReadDecimalAsyncInternal(bool syncRequest)
 {
     do
     {
          if (!await ReadAsync(syncRequest))
          {
               SetInternalProperies()
          }
          else
          {
               return _reader.AsDecimal();
          }
      } while (_reader.hasValue)
}

private Task<bool> ReadAsync(bool syncRequest)
{
    if(syncRequest)
    {
        return Task.FromResult(streamReader.Read())
    }
    else
    {
        return StreamReader.ReadAsync(); 
    }
}

最佳答案

除了库中的同步方法之外,您还想添加异步方法。您链接到的文章正是讨论了这一点。它建议为两个版本创建专门的代码。

现在通常会给出建议,因为:

  • 异步方法应该是低延迟的。为了提高效率,他们应该在内部使用异步 IO。
  • 出于效率原因,同步方法应该在内部使用同步 IO。

  • 如果您创建包装器,您可能会误导调用者。

    现在,它是 有效 如果您同意后果,则以两种方式创建包装器的策略。它当然节省了大量代码。但是您必须决定是优先使用同步版本还是异步版本。另一个将效率较低,并且没有基于性能的存在理由。

    您在 BCL 中很少会发现这一点,因为实现的质量很高。但例如 ADO.NET 4.5 的 SqlConnection类使用异步同步。执行 SQL 调用的成本远大于同步开销。这是一个不错的用例。 MemoryStream使用(某种)async-over-sync 因为它本质上只是 CPU 工作,但它必须实现 Stream .

    实际开销是多少?预计能跑>1亿Task.FromResult每秒和数百万几乎为零的工作 Task.Run每秒。与许多事情相比,这是很小的开销。

    请参阅下面的评论以进行有趣的讨论。 为了保留该内容,我将一些评论复制到此答案中。在复制时,我尽量省略主观评论,因为这个答案是客观真实的。完整的讨论如下。

    可以可靠地避免死锁。例如,ADO.NET 在最新版本中使用异步同步。您可以在查询运行时暂停调试器并查看调用堆栈时看到这一点。众所周知,sync-over-async 是有问题的,这是事实。但是你绝对不能使用它是错误的。这是一种权衡。

    以下模式总是安全的(只是一个例子):Task.Run(() => Async()).Wait(); .这是安全的,因为在没有同步上下文的情况下调用 Async。死锁的可能性通常来自捕获同步上下文的异步方法,它是单线程的,然后想要重新输入它。另一种方法是始终使用 ConfigureAwait(false)这很容易出错(一个错误会在早上 4:00 使您的生产应用程序死锁)。另一种选择是 SetSyncContext(null); var task = Async(); SetSyncContext(previous); .

    我也喜欢 bool 标志的想法。这是另一种可能的权衡。大多数应用程序并不关心以这些小方式优化性能。他们想要正确性和开发人员的生产力。在许多情况下,异步对两者都不利。

    如果您希望以任意方式调用异步方法,则必须使用 ConfigureAwait(false)无论如何,建议用于库代码。然后,您可以使用 Wait()没有危险。我还想指出,异步 IO 不会以任何方式改变实际工作(DB、Web 服务)的速度。它还增加了 CPU 调用开销(更多,而不是更少的开销)。任何性能提升都只能来自增加并行度。同步代码也可以进行并行处理。仅当并行度如此之高以至于无法合理使用线程(数百个)时,异步才具有优势。

    异步可以通过一些其他方式提高性能,但这些方式非常小,并且会出现在特殊情况下。通常,您会发现正常的同步调用速度更快。我知道这是因为我尝试过,也从理论观察中得知。

    当不缺少线程时,保存线程是没有意义的。大多数(不是全部)服务器应用程序在任何方面都不缺少线程。一个线程只有 1MB 的内存和 1ms 的 CPU 开销。通常有足够的线程来处理传入的请求和其他工作。在过去的 20 年里,我们使用同步 IO 对我们的应用程序进行了编程,并且完全没问题。

    我想澄清一下异步同步通常有更多的开销,因为它结合了异步的开销和等待任务的开销。不过,在几乎所有情况下,纯同步调用链比纯异步调用链使用更少的 CPU。但话又说回来,这些微小的性能差异在几乎所有情况下都无关紧要。所以我们应该优化开发人员的生产力。

    异步的好例子是长时间运行和频繁的 IO。此外,还有很大程度的并行性(例如,如果您想通过 TCP 连接到 100 万个聊天客户端,或者您正在查询具有 100 个并行连接的 Web 服务)。在这里,异步 IO 具有有意义的性能和可靠性增益。 Task + await 是一个很棒的实现方式。 await 加上异步 IO 在客户端 GUI 应用程序中也非常好。我不想给人留下我坚决反对异步的印象。

    但是您也可以灵活地从异步过渡出来。例如。 Task.WaitAll(arrayWith100IOTasks)只会烧掉一个等待 100 个并行 IO 的线程。这样您就可以避免感染整个调用堆栈并节省 99 个线程。在 GUI 应用程序中,您经常可以这样做 await Task.Run(() => LotsOfCodeUsingSyncIO()) .同样,只有一个地方感染了 async 并且你有很好的代码。

    关于c# - 在库中编写同步和异步方法并保持 DRY 的模式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27971156/

    相关文章:

    .net - 在 WPF ListBoxItems 中使用 Storyboard动画进行鼠标悬停和选择

    .net - ping/通知 .NET Windows 服务的最简单方法是什么?

    python - 如何在异步方法中将结果存储在列表中?

    c# - 您将如何使用 Scala 的异步使多个异步请求超时?

    c# - 在 Windows 中检测耳机

    c# - Asp.net 核心。使用类型参数配置 IOptions 方法(非通用)

    c# - LINQ: ...Where(x => x.Contains(以 "foo"开头的字符串 ))

    c# - WPF MVVM模式中两个 View 之间的共享 View 模型

    .net - .NET Micro Framework 的 BitArray 替代方案

    Node.js:是否使用 Crypto 模块完全异步创建 MD5 哈希?