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

时间:2015-01-15 19:11:35

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

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

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

我正考虑将TextReader.Read方法包装在Task.Return()中。即使它是一项任务,TPL的改进也不应该让它切换到不同的线程,我仍然可以使用异步等待大多数代码,就像正常一样。那么同步的包装器可以只是Task.Result还是Wait()

我查看了.net库中的其他示例。 StreamReader似乎复制了异步和非异步之间的代码。 MemoryStream执行Task.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(); 
    }
}

2 个答案:

答案 0 :(得分:4)

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

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

  1. 异步方法应该是低延迟的。为了提高效率,他们应该在内部使用异步IO。
  2. 出于效率原因,同步方法应在内部使用同步IO。
  3. 如果您创建包装器,可能会误导来电者。

    现在,如果您对后果感到满意,那么以两种方式创建包装器是一种有效策略。它当然可以节省大量代码。但您必须决定是优先同步还是异步版本。另一个效率较低,没有基于绩效的理由存在。

    您很少在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 :(得分:1)

  

然后可以将同步的包装器设置为Task.Result或Wait()吗?

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

如果您将同步代码与任务包装在一起,那么您就会错失这一优势。此外,当他们假设等待的通话会将控制权交还给来电者时,您会误导您的API来电者。

修改

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

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