这个问题在这里已有答案:
我正在修改库以添加异步方法。从Should I expose synchronous wrappers for asynchronous methods?它说我不应该在调用同步方法时在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)
}
这是一个小例子,但您可以看到唯一的代码更改是等待和任务。
为了说清楚我想在库中的所有位置使用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();
}
}
除了lib中的同步方法之外,您还想添加异步方法。您链接的文章正好谈到了这一点。它建议为这两个版本创建专门的代码。
现在通常会给出建议,因为:
如果您创建包装器,可能会误导调用者。
现在,如果您对结果有好处,那么就可以通过两种方式创建包装器。它当然可以节省大量代码。但您必须决定是优先同步还是异步版本。另一个效率较低,没有基于性能的理由存在。
您很少在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())
。再一次,只有一个感染异步的地方,你有很好的代码。
那么同步的包装器可以只是Task.Result还是Wait()呢?
您必须了解异步IO的全部内容。它不是关于代码重复,而是关于利用当工作自然异步时不需要任何线程这一事实。
如果你用一个任务包装你的同步代码,你就错过了这个优势。此外,当他们假设等待的呼叫会将控制权交还给呼叫者时,您会误导您的API呼叫者。
编辑:
你的例子强化了我的观点。不要从使用任务。同步apis本身是完全正常的,不要在不需要时强制使用TPL,如果它导致你的代码库增加2倍的行数,那就是eveb。
花点时间正确实现异步api。不要阻止异步代码,让它一直流到堆栈的底部。