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


67

在荷兰的Techday期间,Steve Sanderson进行了有关C#5,ASP.NET MVC 4和异步Web的演讲

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

然后,他展示了异步Web请求的使用如何提高性能,因为随后将工作委派给另一个线程,并且线程池可以快速响应新的传入请求。他甚至对此进行了演示,并显示出50个并发请求首先耗时50 * 1s,但是异步行为总共仅1,2 s。

但是看到这个之后,我仍然有一些疑问。

  1. 为什么我们不能只使用更大的线程池?是不是不是使用async / await来使另一个线程变慢,而是仅仅从一开始就增加了线程池?这不像我们运行的服务器突然获得更多线程或其他东西吗?

  2. 来自用户的请求仍在等待异步线程完成。如果池中的线程正在执行其他操作,那么“ UI”线程如何保持繁忙?史蒂夫(Steve)提到了一些“知道何时完成的智能内核”。这是如何运作的?

Answers:


65

这是一个非常好的问题,理解它是理解异步IO为何如此重要的关键。之所以将新的异步/等待功能添加到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倍,达到1,000个线程。

  • 可扩展性和性能:第二个问题是,如果这样做,每个请求仍将使用一个线程。线程是一种昂贵的资源。.NET中的每个托管线程都需要为堆栈分配1 MB的内存。对于持续5秒且每秒负载500个请求的网页,您的线程池中将需要2500个线程,这意味着2.5 GB的内存用于不做任何事情的线程堆栈。然后,您会遇到上下文切换的问题,这将严重损害计算机的性能(影响计算机上的所有服务,而不仅仅是Web应用程序)。即使Windows在忽略等待的线程方面做得相当不错,但它并不是设计来处理如此多的线程。

因此,增加线程池的大小是一种解决方案,而且人们已经这样做了十年(即使在Microsoft自己的产品中),在内存和CPU使用率方面它的可扩展性和效率也较低,而且您始终处于IO延迟突然增加会导致饥饿的摆布。直到C#5.0为止,异步代码的复杂性对于许多人来说都是不值得的麻烦。async / await像现在一样改变了一切,您可以从异步IO的可伸缩性中受益,并同时编写简单的代码。

更多详细信息:http : //msdn.microsoft.com/zh-cn/library/ff647787.aspx当Web服务调用继续进行时,如果有机会执行其他并行处理,请使用异步调用来调用Web服务或远程对象。如果可能的话,应避免同步(阻塞),因为传出调用Web服务通过使用线程从ASP.NET线程池作出调用Web服务。阻塞调用减少可用线程数,用于处理其他传入的请求。


14
此答复未回答问题的第二部分。
nunespascal,2012年

1
关于为什么要转向异步模式的良好理性。
eduncan911

3
我认为这不能解决这样一个事实,即不管I / O是不可预测的,还是有其他规定,用户仍然必须等待所有事情完成后才能获得响应。http / web服务器本身可以处理更多负载的事实并不意味着它能够完全处理请求。除了更改事物的分布方式并可能引入更昂贵的上下文切换之外,我看不到异步如何解决此问题。
nilskp

使用asyc api确实减少了线程数,但是并没有减少上下文切换。上下文切换仍然是相同的。
实用的

进入和退出等待/睡眠/加入状态的线程数量越少,上下文切换的数量就越少。实际上,如果线程数少于CPU上的虚拟核心数(如果没有同步IO则可能),那么就没有上下文切换。
Flavien

32
  1. 异步/等待不是基于线程的;它基于异步处理。在ASP.NET中进行异步等待时,请求线程将返回到线程池,因此在异步操作完成之前,没有线程为该请求提供服务。由于请求开销低于线程开销,因此这意味着异步/等待可以比线程池更好地扩展。
  2. 请求具有大量未完成的异步操作。此计数由的ASP.NET实现管理SynchronizationContext。您可以SynchronizationContext我的MSDN文章中了解更多信息-它涵盖了ASP.NET的SynchronizationContext工作方式和await使用方法SynchronizationContext

在异步/等待之前可以进行ASP.NET异步处理-您可以使用异步页面,并使用EAP组件,例如WebClient(基于事件的异步编程是一种基于的异步编程样式SynchronizationContext)。Async / await也使用SynchronizationContext,但是语法简单。


1
我仍然很难理解,但感谢您提供的信息和您的文章。它稍微澄清了一些问题:)您能解释一下异步处理和线程之间的最大区别吗?我以为如果我在等待时执行一些代码,它将在另一个线程上运行,以便当前线程可以返回到池中。
Wouter de Kort 2012年

2
@WouterdeKortasync使代码异步运行但不启动新线程,就像正在执行当前线程中的代码一样,但是SynchronizationContext它将在异步代码行和方法的其余部分之间交换...
Jalal Said

1
@Wouter异步处理不需要线程。在ASP.NET中,如果您执行await的操作不完整,则会await将该方法的其余部分安排为继续,然后返回。线程返回到线程池,而没有线程为请求提供服务。稍后,当await操作完成时,它将从线程池中取出一个线程,并继续为该线程上的请求提供服务。因此,异步编程不依赖线程。尽管如果需要它也可以很好地与线程配合使用:您可以await使用进行线程池操作Task.Run
斯蒂芬·克莱里

3
@StephenCleary我认为人们遇到的主要问题是:“线程返回到线程池,没有任何线程为请求服务。稍后,当await操作完成时,...”如果没有线程,await操作如何完成是用来处理请求的?什么执行该代码?它不是“自发地”完成的,必须运行某些东西。那是模糊的部分。
弗朗斯·布玛

3
@FransBouma:当我第一次遇到“异步IO”(在研究Node.js时),这也使我感到困扰。经过研究,我发现某些操作可以通过某些设备(例如HD)在硬件级别异步执行。操作系统请求对HD进行读取操作,然后返回执行其他操作。HD本身将获取数据,填充其(物理)缓冲区,然后向处理器发送信号,表明已完成读取。操作系统检测到此情况,然后从池中获取一些线程以继续处理所获取的数据。
拉斐尔2014年

9

试想一下,线程池为一组,你已经雇用的工人工作。您的工作人员为您的代码运行快速的cpu指令。

现在,您的工作恰好取决于另一个慢家伙的工作。慢家伙是磁盘网络。例如,您的工作可以分为两部分,一部分必须在慢男的工作之前执行,而另一部分必须在慢男的工作之后执行。

您将如何建议您的工人完成工作?您是否要对每个工作人员说-“做第一部分,然后等到那个慢家伙完成后再做第二部分”?您是否会因为所有工人似乎都在等待那个慢家伙而又无法满足新客户而增加工人的数量?没有!

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.