我正在修改库以添加异步方法。从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();
}
}
答案 0 :(得分:4)
除了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())
。再一次,只有一个感染异步的地方,你有很好的代码。
答案 1 :(得分:1)
然后可以将同步的包装器设置为Task.Result或Wait()吗?
您必须了解异步IO的全部内容。它不是关于代码重复,而是关于利用当工作自然异步时你不需要任何线程这一事实。
如果您将同步代码与任务包装在一起,那么您就会错失这一优势。此外,当他们假设等待的通话会将控制权交还给来电者时,您会误导您的API来电者。
修改强>
你的例子强化了我的观点。不要使用任务。同步apis本身是完全正常的,不需要在不需要的时候使用TPL,如果它导致你的代码库增加2倍的行数,那就是eveb。
花点时间正确实现异步api。不要阻止异步代码,让它一直流到堆栈的底部。