为什么要使用异步请求而不是使用更大的线程池?

时间:2012-02-26 13:43:23

标签: c# asp.net asynchronous async-ctp

在荷兰的Techdays期间,Steve Sanderson做了关于C#5, ASP.NET MVC 4, and asynchronous Web.

的演讲

他解释说,当请求需要很长时间才能完成时,线程池中的所有线程都会变忙,新请求必须等待。服务器无法处理负载,一切都变慢了。

然后,他展示了如何使用异步Web请求提高性能,因为然后将工作委托给另一个线程,并且线程池可以快速响应新的传入请求。他甚至演示了这一点,并显示50个并发请求首先占用了50 * 1,但异步行为总共只有1,2 s。

但在看到这个之后我还有一些问题。

  1. 为什么我们不能只使用更大的线程池?是不是使用async / await来启动另一个线程,然后从头开始增加线程池?它不像我们运行的服务器突然获得更多线程或什么?

  2. 来自用户的请求仍在等待异步线程完成。如果池中的线程正在执行其他操作,那么“UI”线程如何保持忙碌状态?史蒂夫提到了一个关于'一个知道什么时候完成的智能内核'的东西。这是如何工作的?

3 个答案:

答案 0 :(得分:63)

这是一个非常好的问题,理解它是理解异步IO如此重要的关键。将新的async / await功能添加到C#5.0的原因是为了简化编写异步代码。对服务器上的异步处理的支持并不新鲜,但它自ASP.NET 2.0以来就存在。

就像Steve向您展示的那样,通过同步处理,ASP.NET(和WCF)中的每个请求都从线程池中获取一个线程。他演示的问题是一个众所周知的问题,称为“线程池饥饿”。如果在服务器上创建同步IO,则线程池线程将在IO期间保持阻塞状态(无效)。由于线程池中的线程数存在限制,因此在负载下,这可能导致所有线程池线程被阻塞等待IO,并且请求开始排队,导致响应时间增加。由于所有线程都在等待IO完成,因此您将看到CPU占用率接近0%(即使响应时间通过屋顶)。

你在问什么(为什么我们不能只使用更大的线程池?)是一个非常好的问题。事实上,这就是大多数人到现在为止解决线程池饥饿问题的方法:在线程池上只有更多的线程。来自Microsoft的一些文档甚至表明可以解决线程池饥饿可能发生的情况。这是一个可以接受的解决方案,直到C#5.0,这样做要比将代码重写为完全异步更容易。

但这种方法存在一些问题:

  • 没有值适用于所有情况:您将需要的线程池线程数线性地取决于IO的持续时间和服务器上的负载。不幸的是,IO延迟大多是不可预测的。这是一个例子: 假设您在ASP.NET应用程序中向第三方Web服务发出HTTP请求,这需要大约2秒钟才能完成。您遇到线程池饥饿,因此您决定将线程池大小增加到200个线程,然后它再次开始正常工作。问题是,可能在下周,Web服务将出现技术问题,将响应时间增加到10秒。突然之间,线程池饥饿又回来了,因为线程被阻塞了5倍,所以你现在需要将数量增加5倍,达到1000个线程。

  • 可扩展性和性能:第二个问题是,如果这样做,您仍然会为每个请求使用一个线程。线程是一种昂贵的资源。 .NET中的每个托管线程都需要为堆栈分配1 MB的内存。对于使IO持续5秒并且每秒加载500个请求的网页,您的线程池中将需要2,500个线程,这意味着2.5 GB的内存对于将无所事事的线程堆栈。然后你遇到了上下文切换的问题,这会对你的机器性能产生很大的影响(影响机器上的所有服务,而不仅仅是你的web应用程序)。尽管Windows在忽略等待线程方面做得相当不错,但它并不是为处理如此大量的线程而设计的。请记住,当运行的线程数等于机器上的逻辑CPU数量(通常不超过16)时,可以获得最高效率。

因此,增加线程池的大小是一个解决方案,人们已经这样做了十年(甚至在微软自己的产品中),在内存和CPU使用方面,它的可扩展性和效率都不高,而且你总是受到导致饥饿的IO延迟突然增加的摆布。直到C#5.0,异步代码的复杂性对许多人来说并不值得。 async / await像现在一样改变了一切,你可以从异步IO的可扩展性中受益,同时编写简单的代码。

更多详细信息:http://msdn.microsoft.com/en-us/library/ff647787.aspx当有可能在Web服务调用进行时执行其他并行处理时,使用异步调用来调用Web服务或远程对象。尽可能避免同步(阻塞)调用Web服务,因为使用ASP.NET线程池中的线程进行传出Web服务调用。阻塞调用减少了处理其他传入请求的可用线程数。

答案 1 :(得分:31)

  1. Async / await不基于线程;它基于异步处理。在ASP.NET中执行异步等待时,请求线程将返回到线程池,因此在异步操作完成之前,有 no 线程为该请求提供服务。由于请求开销低于线程开销,这意味着async / await可以比线程池更好地扩展。
  2. 请求包含未完成的异步操作。此计数由SynchronizationContext的ASP.NET实现管理。您可以在my MSDN article中详细了解SynchronizationContext - 它涵盖了ASP.NET的SynchronizationContext如何运作以及await如何使用SynchronizationContext
  3. 在异步/等待之前可以进行ASP.NET异步处理 - 您可以使用异步页面,并使用EAP组件,例如WebClient(基于事件的异步编程是一种基于{{1}的异步编程风格})。 Async / await也使用SynchronizationContext,但更多语法更容易。

答案 2 :(得分:8)

想象一下,线程池是您用来 工作的一组工作者。您的员工为您的代码运行快速 cpu 说明。

现在你的工作恰好依赖于另一个慢人的工作;慢人是磁盘网络。例如,您的工作可以包含两部分,一部分必须在慢工作之前执行,另一部分必须在之后执行小伙子的工作。

您如何建议您的员工做您的工作?你会对每个工人说 - "做这第一部分,然后等到那个慢人完成,然后做你的第二部分" ?你会增加你的工人数量,因为他们似乎都在等待那个慢人,你无法满足新客户的需求?不!

你会要求每个工作人员完成第一部分并要求慢人回来并在完成后将消息放入队列中。您可以告诉每个工作人员(或者可能是工作人员的专用子集)在队列中查找已完成的消息并执行第二部分工作。

上面提到的智能内核是操作系统为慢速磁盘和网络IO完成消息维护此类队列的能力。