每个人都知道异步为您提供“更好的吞吐量”,“可扩展性”,并且在资源消耗方面更高效。在进行下面的实验之前,我也想过这种(简单的)方法。它基本上告诉我们,如果我们考虑异步代码的所有开销并将其与正确配置的同步代码进行比较,那么它几乎不会产生性能/吞吐量/资源消耗优势。
问题:与具有正确配置的线程池的同步代码相比,异步代码实际上执行得更好吗?可能是我的表演测试有些戏剧性的缺陷?
测试设置:两个ASP.NET Web API方法,JMeter尝试使用200个线程线程组(30秒rump up time)调用它们。
[HttpGet]
[Route("async")]
public async Task<string> AsyncTest()
{
await Task.Delay(_delayMs);
return "ok";
}
[HttpGet]
[Route("sync")]
public string SyncTest()
{
Thread.Sleep(_delayMs);
return "ok";
}
这是响应时间(对数刻度)。注意当Thread Pool注入足够的线程时,同步代码如何变得更快。如果我们事先设置线程池(通过SetMinThreads
),它将从一开始就胜过async
。
你会问的资源消耗怎么样? “在CPU时间调度,上下文切换和RAM占用空间方面,线程成本很高”。没那么快。线程调度和上下文切换是有效的。就堆栈使用情况而言,线程不会立即消耗RAM,而只是保留虚拟地址空间并仅提交实际需要的tiny fraction。
我们来看看数据的含义。即使使用更大量的线程,同步版本也具有更小的内存占用(映射到物理内存的工作集)。
UPDATE。我想发布后续实验的结果,这应该更具代表性,因为避免了第一个的一些偏见。
首先,第一个实验的结果是使用IIS Express,它基本上是开发时间服务器,所以我需要远离它。另外,考虑到反馈我从服务器隔离了负载生成机器(同一网络中的两个Azure VM)。我还发现一些IIS线程限制是from hard to impossible违反并最终切换到ASP.NET WebAPI自托管以从变量中消除IIS。请注意,此测试的内存占用量/ CPU时间完全不同,请不要在不同的测试运行中比较数字,因为设置完全不同(托管,硬件,机器设置)。此外,当我转移到另一台机器和另一个托管解决方案时,线程池策略发生了变化(它是动态的)并且注入率增加了。
设置:延迟100ms,200 JMeter“用户”,30秒加速时间。
我想用以下结论总结这些实验:是的,在某些特定的(更像实验室的情况)情况下,可以获得同步与异步的可比结果,但在现实世界中,工作负载不能100%可预测且工作量不均匀我们不可避免地会遇到某种线程限制:服务器端限制或线程池增长限制(并且请记住线程池管理是自动机制,并不总是容易预测的属性)。此外,同步版本确实具有更大的内存占用(工作集和更大的虚拟内存大小)。就CPU消耗而言,async也会获胜(每个请求指标的CPU时间)。
在具有默认设置的IIS上,情况更加引人注目:由于线程数量的严格限制 - 20 per CPU,同步版本的速度较慢(和较小的吞吐量)。
PS。对IO使用异步管道! [... 松口气...]
每个人都知道异步为您提供“更好的吞吐量”,“可扩展性”,并且在资源消耗方面更高效。
可扩展性,是的。吞吐量:取决于。每个异步请求都比等效的同步请求慢,因此只有在可伸缩性发挥作用时才会看到吞吐量优势(即,请求数多于可用线程数)。
与具有正确配置的线程池的同步代码相比,异步代码实际上执行得更好吗?
好吧,那里有“正确配置的线程池”。您所假设的是您可以1)预测您的负载,2)拥有足够大的服务器来处理它,每个请求使用一个线程。对于许多(大多数?)现实世界的生产场景,其中一个或两个都不是真的。
为什么不增加线程池的大小[而不是使用异步]?答案是双重的:异步代码比阻塞线程池线程更进一步和更快地扩展。
首先,异步代码比同步代码进一步扩展。使用更实际的示例代码,ASP.NET服务器的总可伸缩性(压力测试)显示出乘法增加。换句话说,异步服务器可以处理作为同步服务器的连续请求数的几倍(两个线程池都达到该硬件的最大值)。但是,这些实验(不是由我完成的)是针对普通ASP.NET应用程序的预期“实际基线”完成的。我不知道如何将相同的结果转移到noop字符串返回。
其次,异步代码比同步代码缩放得更快。这个很明显;同步代码可以很好地扩展到线程池线程的数量,但是不能比线程注入速率更快地扩展。因此,如果在响应时间图表的开头显示,您可以对突然的重负载做出非常缓慢的响应。
我认为你所做的工作很有意思;我对内存使用的差异(或者更确切地说,缺乏差异)感到特别惊讶。我很乐意看到你把它写进博客文章。建议:
作为最后提醒(也来自我的文章):
请记住,异步代码不会替换线程池。这不是线程池或异步代码;它是线程池和异步代码。异步代码允许您的应用程序充分利用线程池。它需要现有的线程池并将其调高到11。
Trully异步代码(I / O)更具可伸缩性,因为它为其他工作释放线程池线程而不是阻塞它们。因此,对于相同数量的线程,它可以处理更多请求。
但它以更多控制数据结构和更多工作为代价。因此,(除了保存线程池线程)它消耗更多的资源(内存,CPU)。
这完全取决于可用性,而不是性能。