70

在荷兰的 Techdays 期间,Steve Sanderson 做了一个关于C#5、ASP.NET MVC 4 和异步 Web的演讲。

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

然后他展示了使用异步 webrequests 如何提高性能,因为工作随后被委托给另一个线程,并且线程池可以快速响应新的传入请求。他甚至对此进行了演示,并显示 50 个并发请求首先需要 50 * 1 秒,但异步行为总共只需要 1.2 秒。

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

  1. 为什么我们不能只使用更大的线程池?是不是使用 async/await 来更慢地启动另一个线程,然后从一开始就增加线程池?是不是我们运行的服务器突然多了线程之类的?

  2. 来自用户的请求仍在等待异步线程完成。如果池中的线程正在做其他事情,“UI”线程如何保持忙碌?Steve 提到了“一个知道某事何时完成的智能内核”。这是如何运作的?

4

3 回答 3

68

这是一个非常好的问题,理解它是理解为什么异步 IO 如此重要的关键。在 C# 5.0 中添加新的 async/await 功能的原因是为了简化异步代码的编写。但是,对服务器上的异步处理的支持并不是新的,它从 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 秒进行 IO 且每秒负载 500 个请求的网页,您的线程池中将需要 2,500 个线程,这意味着 2.5 GB 的内存可用于无所事事的线程堆栈。然后你会遇到上下文切换的问题,这将对你的机器性能造成严重影响(影响机器上的所有服务,而不仅仅是你的 Web 应用程序)。尽管 Windows 在忽略等待线程方面做得相当好,但它并不是为处理如此大量的线程而设计的。

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

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

于 2012-02-27T13:56:21.213 回答
32
  1. async/await 不是基于线程的;它基于异步处理。在 ASP.NET 中执行异步等待时,请求线程将返回到线程池,因此在异步操作完成之前没有线程为该请求提供服务。由于请求开销低于线程开销,这意味着 async/await 可以比线程池更好地扩展。
  2. 请求具有未完成的异步操作计数。此计数由SynchronizationContext. 您可以SynchronizationContext我的 MSDN 文章中了解更多信息- 它涵盖了 ASP.NET 的SynchronizationContext工作原理以及如何await使用SynchronizationContext.

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

于 2012-02-26T13:56:51.500 回答
9

将线程池想象成您雇用的一组工作人员来完成您的工作。您的工作人员为您的代码运行快速的cpu指令。

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

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

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

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

于 2014-08-11T19:49:05.623 回答