ADO:在管道上一直异步?

时间:2018-04-14 02:12:27

标签: asynchronous async-await ado.net system.io.file

好的,所以" 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.
    }
}

步骤:

  1. 应该是相当无害的,无需担心。

  2. 但是现在我们在可能有限的连接池中获得了一个开放连接。如果在等待第2步时,其他长时间运行的任务位于任务调度程序的行首?

  3. 现在更糟糕的是,我们等待着一个开放的连接(很可能会增加延迟)。
  4. Aren我们是否持有超过必要的连接?这不是一个不受欢迎的结果吗?使用同步方法减少总体连接时间不会更好,最终导致我们的数据驱动应用程序表现更好吗?

    当然我理解async并不意味着更快,但异步方法提供了更多总吞吐量的机会。但正如我所观察到的那样,当等待最终延迟操作的任务之间安排任务时,肯定会有奇怪的,并且由于底层资源的限制,基本上就像阻塞一样。

    [注意:这个问题主要关注ADO,但这也适用于文件读写。]

    希望获得更深入的见解。谢谢。

4 个答案:

答案 0 :(得分:2)

这里有几点需要考虑:

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

  2. 线程池设置。如果加载峰值,线程池将不会快速添加线程。它每500ms左右只会添加一个新线程。请参阅MSDN Threading Guidelines from 2004The CLR Thread Pool 'Thread Injection' Algorithm。这是我的一个项目中忙线程数的捕获。由于缺少可用的线程来处理请求,因此加载的加载和请求被延迟。随着新线程的添加,该行增加。 请记住每个线程的堆栈需要1MB内存。 1000个线程〜= 1GB的RAM仅用于线程。 enter image description here

  3. 项目的负载特征与线程池有关。
  4. 您提供的系统类型,我假设您正在讨论ASP.NET类型的应用程序/ api
  5. 吞吐量(请求/秒)与延迟(秒/请求)要求。 Async会增加延迟但会增加吞吐量。
  6. 数据库/查询性能,与下面的50ms建议有关
  7. 文章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)

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

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

怀疑:

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

高容量同步/阻塞代码有它的缺点,但在很大程度上是由现代线程管理良好:

测试/分析

4 x 100个并行查询,每个查询1000个记录。

同步查询的性能配置文件

CPU Performance Profile for Synchronous Query 平均查询: 00:00:00.6731697 ,总时间: 00:00:25.1435656

具有同步读取的异步设置的性能配置文件

CPU Performance Profile for Async Setup with Synchronous Read 平均查询: 00:00:01.4122918 ,总时间: 00:00:30.2188467

完全异步查询的性能配置文件

CPU Performance Profile for Fully Async Query 平均查询: 00:00:02.6879162 ,总时间: 00:00:32.6702872

评估

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

您可以在结果中轻松查看。我们引入的异步操作越多,查询所用的时间越长,完成的总时间就越长。更糟糕的是,完全异步使用更多的CPU开销,这与使用异步任务提供更多可用线程时间的想法相反。这种开销可能是由于我如何运行这些测试,但以类似的比较方式处理每个测试非常重要。同样,如果有人有办法证明异步更好,请做。

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