每个人都知道异步可以提高“吞吐量”、“可伸缩性”,并且在资源消耗方面更加高效。在进行以下实验之前,我也是这样想的(过于简单化)。基本上,它表明,如果我们考虑异步代码的所有开销并将其与正确配置的同步代码进行比较,则几乎没有性能/吞吐量/资源消耗优势。
问题:与正确配置的线程池的同步代码相比,异步代码是否真的执行得更好?也许我的性能测试有某种重大缺陷?
测试设置:两个 ASP.NET Web API 方法和 JMeter 尝试使用 200 个线程线程组调用它们(30 秒的 ramp up 时间)。
这是响应时间(对数刻度)。注意,当线程池注入足够的线程时,同步代码变得更快。如果我们事先设置了线程池(通过
在IIS上,默认设置下情况更加严峻:由于线程数的相当紧密的限制每个CPU20个线程,同步版本的速度慢了几个数量级(吞吐量更小)。
附:对于IO,请使用异步管道![...松了一口气...]
问题:与正确配置的线程池的同步代码相比,异步代码是否真的执行得更好?也许我的性能测试有某种重大缺陷?
测试设置:两个 ASP.NET Web API 方法和 JMeter 尝试使用 200 个线程线程组调用它们(30 秒的 ramp up 时间)。
[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";
}
这是响应时间(对数刻度)。注意,当线程池注入足够的线程时,同步代码变得更快。如果我们事先设置了线程池(通过
SetMinThreads
),它将从一开始就胜过async
。
你可能会问资源消耗方面的问题。"线程在 CPU 时间调度、上下文切换和 RAM 占用方面成本高昂。" 不要那么快下结论。线程调度和上下文切换是有效的。就栈使用情况而言,线程不会立即占用 RAM,而是只保留虚拟地址空间,并仅提交实际需要的微小部分。
让我们看看数据说了什么。即使有更多的线程,同步版本的内存占用也较小(工作集映射到物理内存中)。
更新。我想发布后续实验结果,这些结果应该更具代表性,因为避免了第一次实验的一些偏差。
首先,第一次实验的结果是使用IIS Express(基本上是开发时间服务器)进行的,所以我需要摆脱它。另外,考虑到反馈,我已经将负载生成机器从服务器中隔离出来(两个位于同一网络中的Azure VM)。我还发现一些IIS线程限制从严格到不可能 被违反,并最终切换到ASP.NET WebAPI自托管来消除IIS变量。请注意,此测试的内存占用/CPU时间与其他测试运行时截然不同,请勿跨不同测试运行比较数字(主机、硬件、机器设置完全不同)。此外,当我转移到另一台机器和另一种托管解决方案时,线程池策略发生了变化(它是动态的),注入率有所增加。
设置:延迟100ms,200个JMeter“用户”,30秒的递增时间。
我想用以下内容总结这些实验:是的,在某些特定(更像实验室)的情况下,同步和异步可以获得可比较的结果,但在现实世界中,工作负载无法100%预测且不均衡的情况下,我们不可避免地会遇到某种线程限制:无论是服务器端限制还是线程池增长限制(请记住,线程池管理是具有不总是易于预测属性的自动机制)。此外,同步版本确实具有更大的内存占用(工作集和虚拟内存大小都更大)。就CPU消耗而言,异步也胜出(每个请求的CPU时间指标)。在IIS上,默认设置下情况更加严峻:由于线程数的相当紧密的限制每个CPU20个线程,同步版本的速度慢了几个数量级(吞吐量更小)。
附:对于IO,请使用异步管道![...松了一口气...]
public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable
,因此一个任务“简单地”说就是一个后台线程。这可能指向你的观点,即一个正确配置的同步方法(我会选择同步后台方法)将具有与任务类似的特性。但是,一旦考虑到这种配置的复杂性,大多数情况下,任务会更加优越。 - peeyush singh