ADO:一直异步?

Posted

技术标签:

【中文标题】ADO:一直异步?【英文标题】:ADO: Async all the way down the tubes? 【发布时间】:2018-09-24 09:59:29 【问题描述】:

好的,所以“一直异步”是任务。但是什么时候有问题呢?

例如,如果您对资源的访问权限有限,例如在 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 步时,其他长时间运行的任务排在任务调度程序的最前面怎么办?

    现在更糟糕的是,我们等待开放连接(并且很可能会增加延迟)。

我们保持打开连接的时间不是比需要的时间长吗?这不是一个不受欢迎的结果吗?使用同步方法来减少整体连接时间不是更好吗,最终使我们的数据驱动应用程序性能更好?

当然,我知道异步并不意味着更快,但异步方法提供了提高总吞吐量的机会。但正如我所观察到的,当在等待之间安排的任务最终会延迟操作,并且由于底层资源的限制而本质上表现为阻塞时,肯定会出现奇怪的情况。

[注意:这个问题的重点是ADO,但这也适用于文件读写。]

希望有更深入的了解。谢谢。

【问题讨论】:

绝对相关:***.com/questions/25086866/… 更多讨论在这里:reddit.com/r/dotnet/comments/8c4ova/… 【参考方案1】:

这里有几点需要考虑:

    数据库连接池限制,具体是“Max Pool Size”,默认为100。数据库连接池有最大连接数上限。请务必设置“Max Pool Size=X”,其中 X 是您希望拥有的最大数据库连接数。这适用于同步或异步。

    线程池设置。如果加载尖峰,线程池将不会快速添加线程。它只会每 500 毫秒左右添加一个新线程。请参阅 MSDN Threading Guidelines from 2004 和 The CLR Thread Pool 'Thread Injection' Algorithm。这是我的一个项目中繁忙线程数的捕获。由于缺少可用线程来服务请求,负载激增并且请求被延迟。随着新线程的添加,该行增加。 请记住,每个线程的堆栈都需要 1MB 内存。 1000 个线程 ~= 1GB RAM 仅用于线程。

    项目的负载特性与线程池有关。 您提供的系统类型,我假设您在谈论 ASP.NET 类型的 app/api 吞吐量(请求/秒)与延迟(秒/请求)要求。异步会增加延迟,但会增加吞吐量。 数据库/查询性能,与下面的 50 毫秒建议有关

文章The overhead of async/await in NET 4.5编辑 2018-04-16 以下建议适用于基于 WinRT UI 的应用程序。

避免对非常短的方法使用 async/await 或使用 await 紧密循环中的语句(改为异步运行整个循环)。 Microsoft 建议任何可能需要超过 50 毫秒的方法 返回应该异步运行,所以你可能希望使用这个 图来确定是否值得使用 async/await 模式。

还可以观看 Diagnosing issues in ASP.NET Core Applications - David Fowler & Damian Edwards 讨论线程池和使用异步、同步等问题。

希望对你有帮助

【讨论】:

很好的答案。谢谢! “50ms 指南”需要上下文:该指南是针对客户端 UI 应用中的 UI 线程给出的。 谢谢斯蒂芬。我错过了中间的一段话,“这是微软在 WinRT API 开发过程中的核心焦点,他们确保任何可能需要超过 50 毫秒才能执行的 API 只能以异步形式提供。”并且只引用了结论中的建议(断章取义)。【参考方案2】:

由于数据库连接池在协议较低级别的工作方式,高级打开/关闭命令对性能没有太大影响。通常,尽管内部线程调度 IO 通常不是瓶颈,除非您有一些非常长时间运行的任务——我们说的是 CPU 密集型或更糟的事情——内部阻塞。这将很快耗尽您的线程池,并且事情将开始排队。

我还建议您调查http://steeltoe.io,尤其是断路器 hystrix 实现。它的工作方式是允许您将代码分组为命令,并由命令组管理命令执行,这些命令组本质上是专用和隔离的线程池。好处是如果你有一个嘈杂的、长时间运行的命令,它只能耗尽它自己的命令组线程池,而不会影响应用程序的其余部分。这部分库还有许多其他优点,主要是断路器实现,以及我个人最喜欢的折叠器之一。想象一下,查询 GetObjectById 的多个传入调用被分组到单个 select * where id in(1,2,3) 查询中,然后将结果映射回单独的入站请求。 Db 调用只是一个例子,实际上可以是任何东西。

【讨论】:

我认为这证实了我的怀疑,即当涉及到 Db 访问时,如果您想确保您的查询尽快进入和退出,不妨坚持使用同步代码.如果您可以同时支持(例如)100 个查询,这似乎适得其反,但由于延迟,每个查询都需要更长的时间。较长的单个查询可能会对您的应用程序产生不利影响,尤其是在使用缓存解决方案时。 并非如此。有一个独立于您的 conn 的底层数据库连接池。开关。如果您不使用异步,则您正在阻塞实际线程,而不是在查询结果可用时依赖回调来恢复代码执行。与异步调用相比,这对您的应用来说代价更高。 对,但我不关心打开/关闭。在使用 .ReadAsync() 进行迭代时,我更担心任务调度程序导致的延迟。您是说如果 1000 个线程调用 .Open() 并随后执行命令,那么在释放连接以供使用时,不会有人告诉您等待?是否有更好的方式来考虑连接池的广度:它只是您可以进行查询的带宽? 唯一会出现异步问题的情况是,如果您遇到线程不足的情况。在这种情况下,您要么将许多长时间运行的 cpu 任务排队,要么阻塞操作。在许多情况下,例如网络访问或数据库,它们通过利用系统中断进入操作系统级别,这些中断本质上是基于事件的。您本质上是说,当我收到有关此中断的信号时,请运行我的任务。您不会将计算线程与等待它联系起来——这是对昂贵资源的巨大浪费。这就是现代代码正在远离此类操作的同步模型的原因【参考方案3】:

如果您对资源的访问权限有限,例如在 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 困难的方式。从更高的水平开始,然后再往低的水平工作是更难的。从较低级别开始并逐步向上工作要容易得多。例如,从 DbConnection.Open / ExecuteReader / Read 之类的任何 I/O 绑定 API 开始,然后首先使这些异步,然后然后async通过你的代码库成长。

【讨论】:

实际情况我非常不同意关于在较高级别同步时切换到较低级别的异步的评论。您需要在转换点调用 .Result 或 .Wait ,这是获得难以调试的死锁的好方法。 olitee.com/2015/01/c-async-await-common-deadlock-scenario 我认为最佳模式是一直异步,直到您调用读取。由于您有一个活动的连接、命令和阅读器,因此 .Read() 应该非常快,几乎没有阻塞。无需潜在地将任务添加到调度程序。还是我这里有概念错误? 因此,在没有任何可见的线程饱和(大量 CPU 开销)的情况下,我的 AsyncFileWriter 示例 (github.com/electricessence/AsyncFileWriter/blob/master/…) 在使用 .WriteAsync() 时完成的时间要长得多(+20 倍)(即使我正确地标记FileStream 是异步的。)其中 .Write() 版本只需要几秒钟。这是推动这个问题的很大一部分。另一个答案的指导似乎是正确的。快速迭代应该保持同步。 @macsux:我并不是说要进行异步同步。我是说当你过渡到异步时,从低处开始工作比从高处开始工作更容易。 (无论哪种方式,你都应该一直异步结束)。 @OrenFerrari:不同的驱动程序在不同的点上是异步的。我建议一路异步。如果您看到使用异步的 20 倍速度差异,我建议 1) 用最少的多余代码复制它,以便显然是异步 API 的错误,然后 2) 将其报告给 Microsoft。 【参考方案4】:

大量的迭代会显着增加延迟和额外的 CPU 使用率

详情请见http://telegra.ph/SqlDataReader-ReadAsync-vs-Read-04-18。

怀疑:

使用异步并非没有成本,需要考虑。 某些类型的操作很适合异步,而另一些则存在问题(出于显而易见的原因)。

大容量同步/阻塞代码有其缺点,但在大多数情况下,现代线程可以很好地管理:

测试/分析

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 开销,这与使用异步任务将提供更多可用线程时间的想法背道而驰。这种开销可能是由于我运行这些测试的方式造成的,但以类似的方式处理每个测试以进行比较很重要。同样,如果有人有办法证明异步更好,请这样做。

我在这里建议“一直异步”有其局限性,应该在某些迭代级别(如文件或数据访问)进行认真审查。

【讨论】:

以上是关于ADO:一直异步?的主要内容,如果未能解决你的问题,请参考以下文章

意图服务中的多个异步 Ado.net sql 插入

tornado6与python3.7,异步新姿势

使用管道时一直异步

Spring Boot - 异步任务

ASP.NET 用AJAX在页面上传头像,不能异步刷新?

9.按键之使用异步通知