【问题标题】:Measuring performance for async vs sync code in ASP.NET Core在 ASP.NET Core 中测量异步与同步代码的性能
【发布时间】:2020-11-05 05:52:46
【问题描述】:

我正在尝试使用 SQL Server Express 和 EF Core 3.1.3 测量 ASP.NET Core 3.1 中异步与同步的性能,并且有两个功能完全相同,除了一个是异步的,一个是同步的:

[HttpGet("search/description/{searchString}")]
public async Task<ActionResult<IEnumerable<Products>>> SearchForProductsDescription(String searchString) {
    return await _context.Products.Where(p => p.Description == searchString).ToListAsync();
}

还有同步版本:

return _context.Products.Where(p => p.Description == searchString).ToList();

我使用 jmeter 作为基准工具,同步函数比异步函数快(如预期的那样),但是当我增加 jmeter 中的线程数使平均响应时间为 500 毫秒时,同步代码是还是更快。我已经尝试在数据库中使用 1000 行和 20000 行,但它仍然更快。我正在尝试找到一种异步函数比同步函数更快但我遇到问题的场景,是否有什么我弄错或误解的地方?

【问题讨论】:

  • 你确定数据库不是瓶颈吗?数据库可以对不同的处理请求一一而不是同时做出反应。这可能是一些锁定。
  • 数据库使用默认的隔离级别READ COMMITTED,除了我没有更改数据库中的任何内容,所以我假设没有锁?
  • async is not a silver bullet。如果您的数据库只有一个 SQL Express 后端,则需要 restrict your web server thread pool size 才能看到 async 的优势。
  • @StephenCleary 我查看了您的链接并尝试在我的 MVC ASP.NET Core 服务器中将 SetMaxThreads 设置为我的 Main() 函数的三行添加并运行我的 jmeter 基准测试与 500 个用户但它和我没用的时候没什么区别。我认为它会限制服务器中的总线程池,但它似乎不起作用,有什么提示吗?
  • @Nygards:我不知道。很多年前,当我编写该代码时,它就起作用了。

标签: c# entity-framework asp.net-core asynchronous benchmarking


【解决方案1】:

从 cmets 看来,您似乎知道 async 允许 Web 服务器通过不显式等待响应而不是加快查询速度来保持响应速度更快。在同步和异步调用之间的并列示例中,异步调用会稍微慢一些,因为需要设置来切换和恢复。所以不会有你可以运行的性能测试来显示任何改进。异步是为了让服务器对负载更敏感。

假设一个 Web 服务器有 100 个工作线程处理请求。每次用户连接到您的网站、刷新页面等时,他们都会被交给工作人员。在任何一个时间点,都可以处理 100 个请求。如果第 101 个请求在所有其他线程都忙于处理请求时出现,可能会发生以下两种情况之一:分配了一个额外的线程(与其他 100 个竞争资源和时间,并且需要时间来分配额外的线程)或请求等待线程被释放。由于您可能会有一些可能需要一秒钟左右的请求操作,因此大多数请求可能需要几毫秒才能运行,所以总的来说,当并发请求超过 100 个时,这只是一个值得注意的问题,因为他们坐着等工人。

假设有一个特定的报告会在数百万行和大量表中产生一些非常强大的 SQL 调用。例如 EOFY 类型的报告或季度报告。这些报告通常不会那么频繁地运行,但是当它们运行时,它可能会在特定时间由相当数量的人运行。 (例如财政年度的第一周)每个查询需要 10+ 秒才能运行,数据库本身只能处理这么多请求,所以第一个查询可能是 10 秒,但会阻止其他查询,因此平均它可能会推动 30 甚至 60 秒.现在,当有 100 个请求进来时,每个线程都会被占用 30 多秒。由于线程已用尽,您的网站变得无响应。异步查询有助于适应这一点。请求仍然需要大约 30 秒,但 Web 服务器释放了侦听工作线程,以便它可以响应其他请求,其中许多不会触发报告,但会在线程池填满时等待。

因此,要实际观察这样的事情,您需要一个特别昂贵的操作,固定可用工作线程的数量,并启动超过这些请求数的负载测试。例如,将 Web 服务器限制为 5 个线程,启动 5 个昂贵的请求,然后启动一个额外的“廉价”请求并测量该廉价请求的响应能力。使用同步代码,该廉价请求将等待释放其中一个线程。使用异步代码,最后一个请求的处理速度会快得多。到目前为止,您的尝试看起来有点缺陷,因为您的所有请求都执行相同的调用并期望性能提升。它不会,它是关于更小、更简单的请求被挂起,等待更长的请求完成。如果您的查询太简单(快速),则没有什么可观察的。您需要一个需要几秒钟才能运行的查询,然后将其与通常需要几毫秒的请求混合。

异步不是网站性能的灵丹妙药。调试起来可能会更难,而且它本身就有点慢。通常,您会看到到处都使用它以保持一致性的示例,或者实际上没有任何好处的简单示例。我的一般建议是默认使用同步调用,并为特别昂贵的操作保存异步调用。例如,通过 ID 加载单个记录我的目标是保持同步,但搜索,特别是涉及文本匹配或涉及非索引列或复杂关联,我将进行异步。通常会有一些例子需要不同的实现,我什至不会依赖异步。例如上面的示例,EOFY 类型的报告可能需要几秒钟才能运行:类似这样的事情,我希望为由显式后台工作程序(或池)处理的报告请求实现一个队列,并使用指向报告副本的 DBContext帮助确保不会并行运行太多请求,并且不会阻止对主应用程序数据库的读写访问。如果大多数请求有可能同时触发该操作,那么简单地在昂贵的查询上打 async 并不会减少它。您可以释放您的 Web 服务器线程池,但仍然会在链的下游造成瓶颈。

【讨论】:

  • 假设我有 10 个线程和 100 个并发请求在 jmeter 中运行。如果我测量相同的数据库调用,但一个是异步的,一个是同步的,并且数据库调用正好需要 100 毫秒,那么异步版本不应该更快,因为同步版本的 10 个线程将等待数据库 100 毫秒,而异步版本可以处理其他请求并为这些请求启动数​​据库调用?还是因为我的数据库是瓶颈,还是因为我在同一台计算机上运行网络服务器和数据库?
  • 我希望在这种情况下,数据库将成为潜在的瓶颈。我不建议将异步用于需要 100 毫秒的调用,因为异步可能会在调用中增加 5-20 毫秒,这是一个非常粗略的估计取决于硬件。从 1 台机器启动的测试场景只会近似来自数百个来源的数百个请求。 IIS 可以花费数百毫秒的时间来引导请求,因此可以轻松掩盖任何预期的差异。要观察类似的情况,您需要在更便宜、更快的请求中混合使用昂贵的异步操作。昂贵的同步请求会被锁定的地方。
  • “响应速度更快”确实意味着更快。是的,在理论场景中,您最多有 100 个线程,所有线程都很忙,并且您收到另一个请求,在另一个请求完成之前它不会得到服务。但如果它“天生较慢”,您将需要 更多 资源来完成相同的(功能性)工作,因此它在现实中永远不会有帮助。
【解决方案2】:

我正在尝试寻找一种异步函数比同步函数更快但我遇到问题的场景,我是否有什么错误或误解?

是的。异步代码不应该更快。它应该需要更少的线程。如果您有 10000 个并发请求,您可能会在同步过程中看到资源(内存、句柄、线程、CPU)耗尽。但在这发生之前,您的数据库就会崩溃。

EF 异步数据访问本身并不是为了提高数据访问的性能,而是为了符合更喜欢异步 IO 的应用程序框架的要求,例如具有单个 UI 线程的桌面应用程序或使用 ASP 的 Web 应用程序。 NET 核心。

EF 中的异步有时对于并行发送多个查询或在您等待长时间运行的数据库操作时运行一些其他代码也很有用。

这是一个综合演示,对于大量并发调用,当线程池小于并发请求数时,异步可以快得多。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncDemo
{
    class Program
    {
        static async Task DoStuffAsync()
        {
            await Task.Delay(20);
        }

        static void DoStuffSync()
        {
            Thread.Sleep(20);
        }
        static void Main(string[] args)
        {
            var sw = new Stopwatch();
            List<Task> tasks;
            var iterations = 1000 * 5;

            while (true)
            {
                sw.Restart();
                tasks = new List<Task>();
                for (int i = 0; i < iterations; i++)
                {
                    tasks.Add(Task.Run(() => DoStuffSync()));
                }
                Task.WaitAll(tasks.ToArray());
                Console.WriteLine($"{iterations} sync calls {sw.ElapsedMilliseconds}ms {Process.GetCurrentProcess().Threads.Count} threads");

                tasks = new List<Task>();
                sw.Restart();
                for (int i = 0; i < iterations; i++)
                {
                    tasks.Add(DoStuffAsync());
                }
                Task.WaitAll(tasks.ToArray());
                Console.WriteLine($"{iterations} async calls {sw.ElapsedMilliseconds}ms {Process.GetCurrentProcess().Threads.Count} threads");

            }

        }
    }
}

输出

5000 sync calls 5546ms 35 threads
5000 async calls 29ms 35 threads
5000 sync calls 3951ms 51 threads
5000 async calls 38ms 51 threads
5000 sync calls 3481ms 52 threads
5000 async calls 29ms 52 threads
5000 sync calls 3320ms 53 threads
5000 async calls 34ms 53 threads
5000 sync calls 3259ms 53 threads
5000 async calls 32ms 53 threads
5000 sync calls 3253ms 53 threads
5000 async calls 33ms 53 threads
5000 sync calls 3321ms 53 threads
5000 async calls 31ms 53 threads
5000 sync calls 3275ms 53 threads
5000 async calls 33ms 53 threads
5000 sync calls 3259ms 51 threads
5000 async calls 28ms 51 threads

【讨论】:

  • 我希望微软能更好地解释这一点。当我第一次了解异步等待时,我在荒野中徘徊了一段时间,直到我意识到异步不是为了提高性能,而是为了释放底层线程,以便诸如 Winforms 和 WPF 中的消息泵之类的东西仍然可以工作。
  • 虽然“符合应用程序框架的要求”这句话在技术上是正确的,但它并没有真正阐明。
  • 我正在开发一个演示 Web 应用程序,并使用了 ASP.NET Core 提供的 MVC 模板。默认情况下,所有函数都是异步的,所以我只是修改了它们以处理我的查询。所以上面的函数基本上是一个端点,由于同步代码正在等待数据库返回,我认为如果它是异步的,线程将被释放并可以处理另一个请求,直到数据库返回,从而提高重负载下的性能,这不是它的工作原理吗?
  • 是的,它就是这样工作的。但是,如果您有足够的线程来处理所有请求等待数据库调用,您将不会看到任何性能差异。通常情况就是这样。
  • 如果我同时使用 2000 个线程(用户)运行 jmeter,我会得到异步线程的平均响应时间为 730 毫秒,同步线程的平均响应时间为 570 毫秒。由于这非常慢且负载很重,这是否意味着异步版本应该优于同步版本?我只是很难理解异步在我的场景中何时会胜过同步,以及为什么模板在速度较慢的情况下使用异步?
猜你喜欢
  • 2018-12-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-24
  • 1970-01-01
  • 1970-01-01
  • 2018-01-23
  • 1970-01-01
相关资源
最近更新 更多