这是一个非常好的问题,理解它是理解为什么异步 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 线程池中的线程进行的。阻塞调用会减少用于处理其他传入请求的可用线程数。 "