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

Posted

技术标签:

【中文标题】为啥使用异步请求而不是使用更大的线程池?【英文标题】:Why use async requests instead of using a larger threadpool?为什么使用异步请求而不是使用更大的线程池? 【发布时间】:2012-03-16 05:59:24 【问题描述】:

在荷兰的 Techdays 期间,Steve Sanderson 做了一个关于C#5, ASP.NET MVC 4, and asynchronous Web. 的演讲

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

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

但是看到这里我还是有一些疑问。

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

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

【问题讨论】:

【参考方案1】:

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

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

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

您会改为让每个工作人员完成第一部分,然后让速度慢的人回来并在完成后将消息放入队列中。您会告诉每个工作人员(或者可能是工作人员的一个专用子集)在队列中查找已完成的消息并执行第二部分工作。

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

【讨论】:

【参考方案2】:

这是一个非常好的问题,理解它是理解为什么异步 IO 如此重要的关键。在 C# 5.0 中添加新的 async/await 功能的原因是为了简化异步代码的编写。对服务器上的异步处理的支持并不是新的,但是它从 ASP.NET 2.0 开始就存在了。

就像 Steve 向您展示的那样,通过同步处理,ASP.NET(和 WCF)中的每个请求都从线程池中获取一个线程。他演示的问题是一个众所周知的问题,称为“thread pool starvation”。如果您在服务器上进行同步 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 的数量(通常不超过 16 个)时,效率最高。

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

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

【讨论】:

此回复未回答问题的第二部分。 很好地解释为什么要转向异步模式。 我认为这并没有解决这样一个事实,即无论 I/O 是不可预测的,也不管有什么其他规定,用户仍然必须等待一切完成才能得到响应。 http/web 服务器本身可以处理更多负载的事实并不意味着它能够完全处理请求。除了改变事物的分布方式并可能引入更昂贵的上下文切换之外,我看不出异步如何解决这个问题。 使用 asyc api 确实减少了线程数,但并没有减少上下文切换。上下文切换仍然是一样的。 进入和退出等待/睡眠/加入状态的线程数越少,上下文切换的次数就越少。事实上,如果线程数小于 CPU 上的虚拟内核数(如果没有同步 IO,则可能出现这种情况),您将没有上下文切换。【参考方案3】:
    异步/等待不是基于线程的;它基于异步处理。当您在 ASP.NET 中执行异步等待时,请求线程将返回到线程池,因此在异步操作完成之前,没有 个线程为该请求提供服务。由于请求开销低于线程开销,这意味着 async/await 可以比线程池更好地扩展。 请求有一个未完成的异步操作计数。此计数由SynchronizationContext 的 ASP.NET 实现管理。您可以在 my MSDN article 中阅读有关 SynchronizationContext 的更多信息 - 它涵盖了 ASP.NET 的 SynchronizationContext 的工作原理以及 await 如何使用 SynchronizationContext

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

【讨论】:

对我来说仍然有点难以理解,但感谢您提供的信息和您的文章。它澄清了一点:)你能解释一下异步处理和线程之间的最大区别是什么吗?我想如果我用 await 执行一些代码,它会在不同的线程上运行,这样当前线程就可以返回到池中。 @WouterdeKort async 使代码异步运行但不启动新线程,就像它正在当前线程中执行代码但 SynchronizationContext 将在异步之间交换代码行和方法的其余部分... @Wouter 异步处理不需要线程。在 ASP.NET 中,如果您 await 一个未完成的操作,那么 await 将安排该方法的其余部分作为延续,并返回。线程返回到线程池,没有线程为请求提供服务。稍后,当await 操作完成时,它将从线程池中取出一个线程并继续为该线程上的请求提供服务。因此,异步编程不依赖于线程。虽然如果你需要它可以很好地处理线程:你可以使用await 使用Task.Run 进行线程池操作。 @StephenCleary 我认为人们的主要问题是:“线程返回到线程池,没有线程为请求提供服务。稍后,当等待操作完成时,......”如何如果没有线程用于处理请求,等待操作是否完成?什么执行该代码?它不会“自发”完成,必须运行它。那是模糊的部分。 @FransBouma:当我第一次遇到“异步 IO”这个术语时(在学习 Node.js 时),这也让我很困扰。经过一番研究,我发现某些设备可以在硬件级别异步执行某些操作,例如 HD。操作系统向 HD 请求读取操作,然后返回做其他事情。 HD 本身将获取数据,填充其(物理)缓冲区,然后向处理器发送一个信号,指示读取完成。操作系统检测到这一点,然后从池中抓取一些线程以继续处理获取的数据。

以上是关于为啥使用异步请求而不是使用更大的线程池?的主要内容,如果未能解决你的问题,请参考以下文章

AsyncContext异步和多线程区别

异步高性能爬虫

异步Action之AsyncController

如何使用异步线程调整/分析线程池配置?

使用JDK1.8 CompletableFuture异步化任务处理

Django异步任务线程池