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

Wou*_*ort 66 c# asp.net asynchronous async-ctp

在荷兰的Techdays期间,Steve Sanderson发表了关于C#5,ASP.NET MVC 4和异步Web的演讲.

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

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

但看到这一点后,我仍然有一些问题.

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

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

Fla*_*ien 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服务.阻塞调用减少可用线程数,用于处理其他传入的请求. "

  • 本回复未回答问题的第二部分. (14认同)
  • 我不认为这解决了这样一个事实,即无论I/O是不可预测的还是其他任何规定,用户仍然必须等待一切都完成才能得到响应.http/web服务器本身可以处理更多负载的事实并不意味着它能够完全处理请求.除了改变事物的分布方式以及可能引入更昂贵的上下文切换之外,我没有看到异步如何解决这个问题. (3认同)

Ste*_*ary 31

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

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

  • @StephenCleary我认为人们遇到的主要问题是:"线程返回到线程池,没有线程为请求提供服务.稍后,当await操作完成时,......"如果没有线程,await操作如何完成?用于处理请求?什么执行该代码?它没有"自发地"完成,必须运行它.那是模糊的部分. (3认同)
  • @FransBouma:当我第一次遇到术语"异步IO"(同时研究Node.js)时,这也困扰了我.经过一些研究,我发现某些操作可以通过某些设备(如HD)在硬件级别异步执行.操作系统向HD请求读取操作,然后返回执行其他操作.HD本身将获取数据,填充其(物理)缓冲区,然后向处理器发送信号,指示读取已完成.操作系统检测到此情况,然后从池中获取某些线程以继续处理获取的数据. (3认同)
  • @WouterdeKort`async`使代码异步运行但不启动新线程,就像它在当前线程中执行代码一样,但`SynchronizationContext`将在异步代码行和方法的其余部分之间交换... (2认同)

tcb*_*tcb 8

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

现在你的工作恰好依赖于另一个慢人的工作; 缓慢的人是磁盘网络.例如,你的工作可以有两部分,一部分必须在慢人工作之前执行,另一部分必须在慢人工作之后执行.

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

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

您在上面提到的智能内核是操作系统维护这种慢速磁盘和网络IO完成消息队列的能力.

  • 很好的解释,真的为我钉了它,谢谢! (3认同)