用于在库中编写同步和异步方法并使其保持干燥的模式[重复]

问题描述 投票:7回答:2

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

例如,库目前使用TextReader.Read方法。部分异步更改我们想使用TextReader.ReadAsync方法。由于这是库的核心,我似乎需要在同步和异步方法之间复制大量代码(希望尽可能保持代码DRY)。或者我需要用PreReadPostRead方法重构它们,这些方法似乎使代码混乱,以及TPL试图解决的问题。

我正在考虑将TextReader.Read方法包装在Task.Return()中。即使它是一项任务,TPL的改进也不应该让它切换到不同的线程,我仍然可以使用异步等待大多数代码,就像正常一样。然后可以将同步的包装器只是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(); 
    }
}
c# .net asynchronous task-parallel-library async-await
2个回答
4
投票

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

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

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

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

现在,如果您对结果有好处,那么就可以通过两种方式创建包装器。它当然可以节省大量代码。但您必须决定是优先同步还是异步版本。另一个效率较低,没有基于性能的理由存在。

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

实际上是什么开销?预计每秒能够运行> 1亿Task.FromResult,每秒运行几百万Task.Run。与许多事情相比,这是一个很小的开销。


有关有趣的讨论,请参阅下面的评论。为了保留这些内容,我将一些评论复制到这个答案中。在复制中,我试图尽可能地忽略主观评论,因为这个答案是客观真实的。完整的讨论如下。

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

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

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

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

还有一些其他方法可以使异步可以提高性能,但这些方法非常小并且会出现特殊情况。通常,您会发现正常的同步调用更快。我知道,因为我尝试了它,也从理论观察。

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

我想澄清一下,异步同步通常会产生更多开销,因为它将异步开销和等待任务的开销结合起来。但是,几乎在所有情况下,纯粹的同步调用链使用的CPU比纯粹的异步调用链少。但是,在几乎所有情况下,这些小的性能差异再次无关紧要。因此我们应优化开发人员的工作效率

异步的好例子是长时间运行且经常运行的IO。此外,还有很大程度的并行性(例如,如果您想通过TCP连接到100万个聊天客户端,或者您正在查询具有100个并行连接的Web服务)。在这里,异步IO具有有意义的性能和可靠性增益。 Task + await是实现这一目标的绝佳方式。 await plus async IO在客户端GUI应用程序中也非常好用。我不想创造一种我绝对反对异议的印象。

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


1
投票

那么同步的包装器可以只是Task.Result还是Wait()呢?

您必须了解异步IO的全部内容。它不是关于代码重复,而是关于利用当工作自然异步时不需要任何线程这一事实。

如果你用一个任务包装你的同步代码,你就错过了这个优势。此外,当他们假设等待的呼叫会将控制权交还给呼叫者时,您会误导您的API呼叫者。

编辑:

你的例子强化了我的观点。不要从使用任务。同步apis本身是完全正常的,不要在不需要时强制使用TPL,如果它导致你的代码库增加2倍的行数,那就是eveb。

花点时间正确实现异步api。不要阻止异步代码,让它一直流到堆栈的底部。

© www.soinside.com 2019 - 2024. All rights reserved.