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

在荷兰的Techdays期间,Steve Sanderson就C#5,ASP.NET MVC 4和asynchronousWeb进行了演示。

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

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

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

  1. 为什么我们不能只使用一个更大的线程池? 不使用asynchronous/等待来调出另一个线程较慢,然后从一开始就增加线程池? 这不像我们运行的服务器突然得到更多的线程或东西?

  2. 来自用户的请求仍然在等待asynchronous线程完成。 如果池中的线程正在做其他事情,“UI”线程如何保持忙碌? 史蒂夫提到了“一个聪明的内核,知道什么时候完成”。 这个怎么用?

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

就像Steve在同步处理中向您展示的那样,ASP.NET(和WCF)中的每个请求都从线程池获取一个线程。 他演示的问题是一个众所周知的问题,称为“ 线程池饥饿 ”。 如果在服务器上创build同步IO,则线程池线程将在IO持续时间内保持阻塞(无所事事)。 由于线程池中的线程数量有限,所以在加载的情况下,这可能导致所有线程池线程都被阻塞,等待IO,并且请求开始排队,导致响应时间增加。 由于所有线程都在等待IO完成,所以CPU占用率接近0%(尽pipe响应时间已经过去了)。

你在问什么( 为什么我们不能只使用一个更大的线程池? )是一个非常好的问题。 事实上,这就是大多数人到现在为止一直在解决线程池饥饿问题:线程池中只有更多的线程。 微软的一些文档甚至指出,当线程池可能发生饥饿时,可以解决这种情况。 这是一个可以接受的解决scheme,在C#5.0之前,要比重写代码完全asynchronous更容易。

这个方法有几个问题:

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

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

因此,增加线程池的大小是一个解决scheme,而且人们已经这样做了十年(即使在微软自己的产品中),就内存和CPU使用而言,它的可扩展性和效率都不高。 IO延迟的突然增加会造成饥饿的怜悯。 直到C#5.0之前,asynchronous代码的复杂性对许多人来说都是不值得的。 async / await会像现在这样改变一切,你可以从asynchronousIO的可伸缩性中受益,同时编写简单的代码。

更多详细信息: http : //msdn.microsoft.com/zh-cn/library/ff647787.aspx “ 当Web服务调用继续时,如果有机会执行额外的并行处理,则使用asynchronous调用来调用Web服务或远程对象。在可能的情况下,避免对Web服务进行同步(阻塞)调用,因为使用ASP.NET线程池中的线程进行传出Web服务调用。阻塞调用会减less处理其他传入请求的可用线程数。

  1. asynchronous/等待不是基于线程; 它基于asynchronous处理。 当您在ASP.NET中进行asynchronous等待时,请求线程将返回到线程池,因此在asynchronous操作完成之前, 没有线程可以处理该请求。 由于请求开销低于线程开销,这意味着asynchronous/等待可以比线程池更好地扩展。
  2. 请求具有未完成asynchronous操作的计数。 此计数由SynchronizationContext的ASP.NET实现pipe理。 您可以在我的MSDN文章中阅读有关SynchronizationContext更多信息 – 它涵盖了ASP.NET的SynchronizationContext如何工作以及await如何使用SynchronizationContext

在async / await之前,可以使用ASP.NETasynchronous处理 – 您可以使用asynchronous页面,并使用WebClient等EAP组件(基于事件的asynchronous编程是基于SynchronizationContext的asynchronous编程风格)。 asynchronous/等待也使用SynchronizationContext ,但语法简单。

想象一下,线程池是一群你已经用来做你的工作的工人。 你的工作人员为你的代码运行快速的cpu指令。

现在你的工作正好取决于另一个缓慢的家伙的工作; 缓慢的家伙是磁盘networking 。 例如,你的工作可以分为两部分,一部分是在慢慢的工作之前执行的,另一部分是在慢慢的工作之后执行的部分。

你如何build议你的工人做你的工作? 你会对每一个工作人员说:“先做这件事,然后等待那个慢慢的家伙完成,然后做你的第二部分”? 你会增加你的工人的数量,因为他们似乎都在等待那个缓慢的家伙,你不能满足新的客户? 没有!

而是要求每个工作人员做第一部分,并要求这个缓慢的人回来,并在完成时将消息放入队列中。 您会告诉每个工作人员(或者可能是一个专门的工人子集)在队列中查找已完成的消息,并进行第二部分工作。

上面提到的智能内核是操作系统维护缓慢磁盘和networkingIO完成消息的队列的能力。