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

Cha*_*ice 7 .net c# asynchronous task-parallel-library async-await

我正在修改库以添加异步方法.From 我应该为异步方法公开同步包装器吗?它说我不应该只是Task.Result在调用同步方法时写一个包装器.但是,我如何在异步方法和同步方法之间复制大量代码,因为我们希望在库中保留两个选项?

例如,库当前使用TextReader.Read方法.我们想要使用TextReader.ReadAsync方法的部分异步更改.由于这是库的核心,我似乎需要在同步和异步方法之间复制大量代码(希望尽可能保持代码DRY).或者,我需要重构出来的PreRead,并PostRead似乎杂乱的代码,什么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)
  }
Run Code Online (Sandbox Code Playgroud)

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

为了说清楚,我想在库中的所有地方使用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(); 
    }
}
Run Code Online (Sandbox Code Playgroud)

usr*_*usr 6

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

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

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

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

现在,它是一个有效的,如果你的后果确定以创建包装左右逢源的策略.它当然可以节省大量代码.但您必须决定是优先同步还是异步版本.另一个效率较低,没有基于性能的理由存在.

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

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


Yuv*_*kov 1

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

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

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

编辑:

你的例子强化了我的观点。不要从使用任务中。同步 api 本身完全没问题,在不需要时不要强制使用 TPL,甚至会导致代码库的行数增加 2 倍。

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

  • @CharlesNRice 除了库中的同步方法之外,您还想添加异步方法,对吧?您链接到的文章正是讨论了这一点。不要在同步包装器上添加异步,或者反之亦然。为什么文章没有回答你的问题? (2认同)