好的,所以" async一直向下"是任务。但什么时候有问题?
例如,如果您对资源的访问权限有限,例如在DbConnection或文件中,您何时停止使用异步方法来支持同步?
让我们回顾一下异步数据库调用的复杂性:
(不要将.ConfigureAwait(false)
放在可读性上。)
// Step 1: Ok, no big deal, our connection is closed, let's open it and wait.
await connection.OpenAsync();
// Connection is open! Let's do some work.
// Step 2: Acquire a reader.
using(var reader = await command.ExecuteReaderAsync())
{
// Step 3: Start reading results.
while(await reader.ReadAsync())
{
// get the data.
}
}
步骤:
应该是相当无害的,无需担心。
但是现在我们在可能有限的连接池中获得了一个开放连接。如果在等待第2步时,其他长时间运行的任务位于任务调度程序的行首?
Aren我们是否持有超过必要的连接?这不是一个不受欢迎的结果吗?使用同步方法减少总体连接时间不会更好,最终导致我们的数据驱动应用程序表现更好吗?
当然我理解async并不意味着更快,但异步方法提供了更多总吞吐量的机会。但正如我所观察到的那样,当等待最终延迟操作的任务之间安排任务时,肯定会有奇怪的,并且由于底层资源的限制,基本上就像阻塞一样。
[注意:这个问题主要关注ADO,但这也适用于文件读写。]
希望获得更深入的见解。谢谢。
答案 0 :(得分:2)
这里有几点需要考虑:
数据库连接池限制,特别是"最大池大小"默认为100.数据库连接池具有最大连接数的上限。设置" Max Pool Size = X"其中X是您希望拥有的最大数据库连接数。这适用于同步或异步。
线程池设置。如果加载峰值,线程池将不会快速添加线程。它每500ms左右只会添加一个新线程。请参阅MSDN Threading Guidelines from 2004和The CLR Thread Pool 'Thread Injection' Algorithm。这是我的一个项目中忙线程数的捕获。由于缺少可用的线程来处理请求,因此加载的加载和请求被延迟。随着新线程的添加,该行增加。 请记住每个线程的堆栈需要1MB内存。 1000个线程〜= 1GB的RAM仅用于线程。
文章The overhead of async/await in NET 4.5 编辑2018-04-16 以下建议适用于基于WinRT UI的应用程序。
避免使用async / await用于非常短的方法或等待 紧密循环中的语句(异步运行整个循环)。 Microsoft建议任何方法可能需要超过50毫秒 返回应该异步运行,所以你可能希望使用它 想确定是否值得使用async / await模式。
还可以使用手表Diagnosing issues in ASP.NET Core Applications - David Fowler & Damian Edwards来讨论线程池问题并使用异步,同步等。
希望这会有所帮助
答案 1 :(得分:1)
由于数据库连接池在较低协议级别的工作方式,高级别的打开/关闭命令不会对性能产生很大影响。通常虽然内部线程调度IO通常不是瓶颈,除非你有一些非常长时间运行的任务 - 我们正在谈论CPU密集型或更糟糕的事情 - 内部阻塞。这将很快耗尽您的线程池,事情将开始排队。
我还建议您调查http://steeltoe.io,特别是断路器hystrix实施。它的工作方式是允许您将代码分组为命令,并由命令组管理命令执行,命令组本质上是专用和隔离的线程池。优点是如果你有一个嘈杂,长时间运行的命令,它只能耗尽它自己的命令组线程池,而不会影响应用程序的其余部分。这部分库有许多其他优点,主要是断路器实现,以及我个人最喜欢的一个折叠器。想象一下,查询的多个传入调用将GetObjectById分组为单个select * where id in(1,2,3)
查询,然后将结果映射回单独的入站请求。 Db电话只是一个例子,可以是真的。
答案 2 :(得分:1)
如果您对资源的访问权限有限,例如在DbConnection或文件中,您何时停止使用异步方法来支持同步?
您根本不需要切换到同步。一般来说,async
只有在一直使用时才有效。 Async-over-sync is an antipattern
考虑异步代码:
using (connection)
{
await connection.OpenAsync();
using(var reader = await command.ExecuteReaderAsync())
{
while(await reader.ReadAsync())
{
}
}
}
在此代码中,在执行命令并读取数据时,连接保持打开状态。只要代码在数据库上等待响应,调用线程就会被释放以执行其他工作。
现在考虑同步等价物:
using (connection)
{
connection.Open();
using(var reader = command.ExecuteReader())
{
while(reader.Read())
{
}
}
}
在此代码中,在执行命令并读取数据时,连接保持打开状态。只要代码在数据库上等待响应,就会阻塞调用线程。
使用这两个代码块,在执行命令并读取数据时,连接保持打开状态。唯一的区别是使用async
代码,调用线程被释放以执行其他工作。
如果在等待第2步时,其他长时间运行的任务位于任务计划程序的行首?
处理线程池耗尽的时间是你遇到它的时候。在绝大多数情况下,它不是问题,默认启发式工作正常。
如果您在任何地方使用async
并且不混合使用阻止代码,则尤其如此。
例如,这段代码会更成问题:
using (connection)
{
await connection.OpenAsync();
using(var reader = command.ExecuteReader())
{
while(reader.Read())
{
}
}
}
现在你有了异步代码,当它恢复时,阻塞 I / O上的线程池线程。做了很多,你最终可能会遇到一个线程池耗尽的情况。
现在更糟糕的是,我们等待着一个开放的连接(很可能会增加延迟)。
增加的延迟微乎其微。像亚毫秒(假设没有线程池耗尽)。与随机网络波动相比,它是无法估量的。
我们不是持有超过必要的连接吗?这不是一个不受欢迎的结果吗?使用同步方法减少总体连接时间不是更好,最终导致我们的数据驱动应用程序表现更好吗?
如上所述,同步代码会保持连接打开一样长。 (嗯,好吧,减去亚毫秒量,但这不重要)。
但正如我所观察到的那样,当等待最终延迟操作的任务之间安排任务时,肯定会有奇怪的,并且由于底层资源的限制,基本上表现得像阻塞一样。
如果您在线程池中观察到这一点,那将会令人担忧。这意味着你已经处于线程池耗尽状态,你应该仔细检查你的代码并删除阻塞调用。
如果您在单线程调度程序(例如,UI线程或ASP.NET Classic请求上下文)中观察到这一点,则不必担心。在这种情况下,您没有处于线程池耗尽状态(尽管您仍需要仔细检查代码并删除阻塞调用)。
作为结束语,听起来好像你试图以艰难的方式添加async
。从更高的层次开始并以更低的水平向前发展是更难的。从较低级别开始并逐步提升起来要容易得多。例如,从任何I / O绑定的API开始,例如DbConnection.Open
/ ExecuteReader
/ Read
,并使那些异步首先,然后 让async
通过您的代码库成长。
答案 3 :(得分:0)
有关详细信息,请参阅http://telegra.ph/SqlDataReader-ReadAsync-vs-Read-04-18。
使用async并非没有成本,需要考虑。 某些类型的操作很适合异步,而其他类型的操作则存在问题(原因应该是显而易见的原因)。
高容量同步/阻塞代码有它的缺点,但在很大程度上是由现代线程管理良好:
4 x 100个并行查询,每个查询1000个记录。
平均查询: 00:00:00.6731697 ,总时间: 00:00:25.1435656
平均查询: 00:00:01.4122918 ,总时间: 00:00:30.2188467
平均查询: 00:00:02.6879162 ,总时间: 00:00:32.6702872
以上结果使用.NET Core 2控制台应用程序在SQL Server 2008 R2上运行。我邀请任何有权访问SQL Server现代实例的人复制这些测试以查看趋势是否存在逆转。如果您发现我的测试方法存在缺陷,请发表评论,以便我更正并重新测试。
您可以在结果中轻松查看。我们引入的异步操作越多,查询所用的时间越长,完成的总时间就越长。更糟糕的是,完全异步使用更多的CPU开销,这与使用异步任务提供更多可用线程时间的想法相反。这种开销可能是由于我如何运行这些测试,但以类似的比较方式处理每个测试非常重要。同样,如果有人有办法证明异步更好,请做。
我在这里建议"一直异步"有它的局限性,应该在某些迭代级别(如文件或数据访问)进行认真审查。