ADO:一直异步吗?

ADO: Async all the way down the tubes?

好的,所以 "async all the way down" 是授权。但是什么时候有问题呢?

例如,如果您对资源的访问权限有限,例如在 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.
    }
}

步骤:

  1. 应该是无害的,不用担心。

  2. 但是现在我们已经在可能有限的连接池中获取了一个打开的连接。如果在等待第 2 步时,其他 运行 长的任务在任务调度器中处于行首怎么办?

  3. 现在更糟糕的是,我们等待一个打开的连接(很可能会增加延迟)。

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

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

[注:本题针对ADO,但也适用于文件读写。]

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

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

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

这里有几点需要考虑:

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

  2. 线程池设置。如果您加载尖峰,线程池将不会快速添加线程。它只会每 500 毫秒左右添加一个新线程。参见 MSDN Threading Guidelines from 2004 and The CLR Thread Pool 'Thread Injection' Algorithm. Here is a capture of the number of busy threads on one of my projects. The load spiked and requests were delayed due to lack of available threads to service the requests. The line increases as new threads were being added. Remember every thread required 1MB of memory for its stack. 1000 threads ~= 1GB of RAM just for threads.

  3. 你项目的负载特性,跟线程池有关。
  4. 您提供的系统类型,我假设您在谈论 ASP.NET 类型 app/api
  5. 吞吐量 (requests/sec) 与延迟 (sec/request) 要求。异步会增加延迟但会增加吞吐量。
  6. database/query 性能,与下面的 50 毫秒建议有关

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

Avoid using async/await for very short methods or having await statements in tight loops (run the whole loop asynchronously instead). Microsoft recommends that any method that might take longer than 50ms to return should run asynchronously, so you may wish to use this figure to determine whether it’s worth using the async/await pattern.

另请关注Diagnosing issues in ASP.NET Core Applications - David Fowler & Damian Edwards,讨论线程池和使用异步、同步等问题

希望这对您有所帮助

if you have limited access to a resource, as in a DbConnection or a file, when do you stop using async methods in favor of synchronous?

您根本不需要切换到同步。一般来说,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 代码,调用线程可以腾出时间去做其他工作。

What if when waiting for step 2, other long running tasks are at the head of the line in the task scheduler?

处理线程池耗尽的时间是您 运行 进入它的时候。在绝大多数情况下,这不是问题,默认启发式算法工作正常。

如果您在任何地方都使用 async 并且不混入阻塞代码,则尤其如此。

例如,这段代码会比较有问题:

using (connection)
{
  await connection.OpenAsync();
  using(var reader = command.ExecuteReader())
  {
    while(reader.Read())
    {
    }
  }
}

现在您有了异步代码,当它恢复时,阻塞 I/O 上的线程池线程。经常这样做,您最终可能会陷入线程池耗尽的境地。

Even worse now, we await with an open connection (and most likely added latency).

增加的延迟很小。像亚毫秒(假设没有线程池耗尽)。与随机网络波动相比,它小得无法估量。

Aren't we holding open a connection longer than necessary? Isn't this an undesirable result? Wouldn't it be better to use synchronous methods to lessen the overall connection time, ultimately resulting in our data driven application performing better?

如上所述,同步代码将保持连接打开的时间一样长。 (好吧,少了亚毫秒的数量,但这并不重要)。

But as I've observed, there can definitely be weirdness when there are tasks scheduled in-between awaits that ultimately delay the operation, and essentially behave like blocking because of the limitations of the underlying resource.

如果您在线程池上观察到这一点,那将是令人担忧的。这意味着您已经处于线程池耗尽状态,您应该仔细检查您的代码并删除阻塞调用。

如果您在单线程调度程序(例如,UI 线程或 ASP.NET 经典请求上下文)上观察到这一点,则不必担心。在那种情况下,您不会耗尽线程池(尽管您仍然需要仔细检查您的代码并删除阻塞调用)。


作为结论,听起来您似乎在尝试以困难的方式添加 async。从较高的层次开始,然后逐步进入较低的层次是比较困难的。从较低级别开始并逐步升级要容易得多。例如,从任何 I/O 绑定的 API 开始,如 DbConnection.Open / ExecuteReader / Read,并使这些异步 first,然后 然后async通过你的代码库成长。

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

有关详细信息,请参阅 http://telegra.ph/SqlDataReader-ReadAsync-vs-Read-04-18

怀疑:

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

大容量 synchronous/blocking 代码有它的缺点,但大多数情况下现代线程管理得很好:

测试/分析

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

评估

以上结果是 运行 在 SQL Server 2008 R2 上使用 .NET Core 2 控制台应用程序得到的。我邀请任何有权访问 SQL 服务器的现代实例的人复制这些测试,看看趋势是否出现逆转。如果您发现我的测试方法有问题,请评论,以便我更正并重新测试。

正如您可以在结果中轻松看到的那样。我们引入的异步操作越多,查询花费的时间就越长,完成的总时间也就越长。更糟糕的是,完全异步使用更多 CPU 开销,这与使用异步任务将提供更多可用线程时间的想法适得其反。这种开销可能是由于我 运行 进行这些测试的方式造成的,但是以类似的方式对待每个测试以进行比较很重要。同样,如果有人有办法证明异步更好,请这样做。

我在这里建议 "async all the way" 有其局限性,应该在某些迭代级别(如文件或数据访问)认真审查。