2012-02-26 75 views
63

的Techdays这里在荷兰的史蒂夫·桑德森在了一个约C#5, ASP.NET MVC 4, and asynchronous Web.为什么使用异步请求而不是使用较大的线程池?

他解释说,当请求需要很长的时间才能完成,全部由线程池线程变得忙碌,新的请求必须等待演示。服务器无法处理负载,一切都变慢。

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

但看到这个后,我仍然有一些问题。

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

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

回答

55

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

就像史蒂夫告诉你的,通过同步处理,ASP.NET(和WCF)中的每个请求都从线程池获取一个线程。他演示的问题是一个众所周知的问题,称为“线程池饥饿”。如果您在服务器上创建同步IO,则线程池线程将在IO持续时间内保持阻塞状态(无需执行任何操作)。由于线程池中的线程数量有限制,所以在负载下,这可能会导致所有线程池线程都被阻塞等待IO的情况,并且请求开始排队,导致响应时间增加。由于所有线程都在等待IO完成,所以您将看到CPU占用率接近0%(即使响应时间已经过去了)。

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

没有与方法的几个问题,但:

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

  • 可扩展性和性能:第二个问题是,如果你这样做,你仍然会为每个请求使用一个线程。线程是一个昂贵的资源。 .NET中的每个托管线程都需要为该堆栈分配1 MB的内存。对于持续5秒制作IO的网页,每秒加载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服务,因为传出的Web服务调用是通过使用ASP.NET线程池中的线程来完成的。阻塞调用减少了处理其他传入请求的可用线程数。

+14

此回复不回答问题的第二部分。 – nunespascal 2012-06-12 04:23:36

+1

为什么转向异步模式的理由很合理。 – eduncan911 2012-11-03 00:47:48

+3

我不认为这解决了这样一个事实,即不管I/O是不可预测的,无论其他规定如何,用户在获得响应之前都必须等待一切完成。 http/web服务器本身可以处理更多负载的事实并不意味着它能够完全处理请求。我没有看到async如何解决这个问题,除了改变事物的分布方式以及可能引入更昂贵的上下文切换。使用asyc api的 – nilskp 2012-11-19 20:50:30

29
  1. 异步/伺机不是基于线程;它基于异步处理。当您在ASP.NET中进行异步等待时,请求线程返回到线程池,因此有异步服务该线程池的线程,直到异步操作完成。由于请求开销低于线程开销,这意味着异步/等待可以比线程池更好地扩展。
  2. 请求具有未完成异步操作的计数。此计数由SynchronizationContext的ASP.NET实现管理。您可以在my MSDN article中阅读有关SynchronizationContext的更多信息 - 它涵盖了ASP.NET的SynchronizationContext如何工作以及await如何使用SynchronizationContext

ASP.NET异步处理,有可能之前异步/ AWAIT - 你可以使用异步页面,并使用EAP组件,如WebClient(基于事件的异步编程是一种基于SynchronizationContext异步编程风格)。异步/等待也使用SynchronizationContext,但有一个很多语法。

+1

这对我来说还是有点难理解,但是感谢您的信息和您的文章。它澄清了一些事情:)你能解释异步处理和线程之间的巨大差异吗?我认为,如果我执行了一些代码,并等待它运行在不同的线程上,那么当前线程可以返回到池中。 – 2012-02-26 14:38:57

+2

@WouterdeKort'async'使代码异步运行,但不启动一个新线程,就像它正在执行当前线程中的代码一样,但是'SynchronizationContext'将在异步代码行和其余的方法... – 2012-02-26 14:54:50

+1

@Wouter异步处理不需要线程。在ASP.NET中,如果您正在等待一个未完成的操作,那么'await'会将该方法的其余部分安排为延续,然后返回。该线程将返回到线程池,而不会为请求提供服务。之后,当await操作完成时,它将从线程池中取出一个线程并继续为该线程上的请求提供服务。所以,异步编程不依赖于线程。虽然如果你需要的话,它可以和线程一起工作:你可以使用Task.Run来“等待”线程池操作。 – 2012-02-27 04:07:17

6

想象的线程池为一组,你已经雇用的工作的工人。你的工人跑得快CPU指令代码。

现在你的工作情况依赖于另一个慢家伙的工作;慢家伙作为网络举例来说,你的工作可以有两个部分,即具有之前执行慢家伙的工作的一部分,并具有以电子邮件的一部分。执行之后缓慢的家伙的工作。

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

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

智能内核您所指的是操作系统维护缓慢磁盘和网络IO完成消息的队列的能力。

+2

很好的解释,真的为我钉了,谢谢! – 2016-07-21 14:18:45