ASP.NET MVC中的asynchronous操作是否使用.NET 4上的ThreadPool的线程

在这个问题之后,在ASP.NET MVC中使用asynchronous操作时,这让我感觉很舒服。 所以,我写了两篇博文:

  • 我在C#5.0和ASP.NET MVC Web应用程序中采用基于任务的asynchronous编程
  • 在ASP.NET MVC 4中使用基于任务的asynchronous编程模型(TAP)调用asynchronous数据库

我对ASP.NET MVC的asynchronous操作有太多的误解。

我总是听到这样一句话: 如果操作asynchronous运行,应用程序可以更好地扩展

而且我也听到过很多这样的句子: 如果stream量很大,最好不要asynchronous执行查询 – 消耗2个额外的线程来处理一个请求,而这些请求会将资源从其他请求中分离出来。

我认为这两句话是不一致的。

我没有太多关于如何在ASP.NET上使用线程池的信息,但我知道线程池的线程大小是有限的。 所以,第二句话必须与这个问题有关。

我想知道ASP.NET MVC中的asynchronous操作是否使用.NET 4上的ThreadPool的线程?

例如,当我们实现一个AsyncController时,应用程序结构如何? 如果我得到巨大的stream量,实施AsyncController是一个好主意吗?

有没有人可以把这个黑色的窗帘放在眼前,向我解释有关ASP.NET MVC 3(NET 4)的asynchronous处理?

编辑:

我已经阅读了下面这个文件近百次,我理解了这个主要的协议,但是我仍然有困惑,因为那里有太多不一致的评论。

在ASP.NET MVC中使用asynchronous控制器

编辑:

假设我有像下面那样的控制器动作(不是AsyncController的实现):

 public ViewResult Index() { Task.Factory.StartNew(() => { //Do an advanced looging here which takes a while }); return View(); } 

正如你在这里看到的,我发起了一次手术,忘记了这一切。 然后,我立即返回而不用等待它完成。

在这种情况下,这是否必须使用线程池中的线程? 如果是这样,完成后,该线程会发生什么? GC完成后是否进入并清理?

编辑:

对于@ Darin的回答,下面是一个与数据库交谈的asynchronous代码示例:

 public class FooController : AsyncController { //EF 4.2 DbContext instance MyContext _context = new MyContext(); public void IndexAsync() { AsyncManager.OutstandingOperations.Increment(3); Task<IEnumerable<Foo>>.Factory.StartNew(() => { return _context.Foos; }).ContinueWith(t => { AsyncManager.Parameters["foos"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); Task<IEnumerable<Bars>>.Factory.StartNew(() => { return _context.Bars; }).ContinueWith(t => { AsyncManager.Parameters["bars"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); Task<IEnumerable<FooBar>>.Factory.StartNew(() => { return _context.FooBars; }).ContinueWith(t => { AsyncManager.Parameters["foobars"] = t.Result; AsyncManager.OutstandingOperations.Decrement(); }); } public ViewResult IndexCompleted( IEnumerable<Foo> foos, IEnumerable<Bar> bars, IEnumerable<FooBar> foobars) { //Do the regular stuff and return } } 

这里有一篇很好的文章,我build议您阅读,以便更好地理解ASP.NET中的asynchronous处理(这是asynchronous控制器基本表示的内容)。

我们先来看一个标准的同步操作:

 public ActionResult Index() { // some processing return View(); } 

当对这个动作发出一个请求的时候,线程池中将会有一个线程被绘制,并且这个动作的主体在这个线程上被执行。 所以如果这个动作里面的处理速度很慢,那么整个处理都会阻塞这个线程,所以这个线程不能被重用来处理其他的请求。 在请求执行结束时,线程返回到线程池。

现在我们来看一个asynchronous模式的例子:

 public void IndexAsync() { // perform some processing } public ActionResult IndexCompleted(object result) { return View(); } 

当一个请求被发送到Index动作时,一个线程被从线程池中抽取出来,并且IndexAsync方法的主体被执行。 一旦这个方法的主体完成执行,线程就返回到线程池。 然后,使用标准的AsyncManager.OutstandingOperations ,一旦你发出完成asynchronous操作的信号,就从线程池中提取另一个线程, IndexCompleted在其上执行IndexCompleted操作的主体,并将结果呈现给客户端。

所以我们在这个模式中可以看到一个客户端的HTTP请求可以被两个不同的线程执行。

现在,有趣的部分发生在IndexAsync方法中。 如果你在里面有一个阻塞操作,你完全在浪费asynchronous控制器的全部目的,因为你阻塞了工作线程(记住这个动作的主体是在从线程池中抽取的线程上执行的)。

那么,我们什么时候才能真正利用asynchronous控制器,你可能会问?

恕我直言,当我们有I / O密集型操作(比如对远程服务的数据库和networking调用)时,我们可以获得最多的收益。 如果你有一个CPU密集型操作,asynchronous操作不会给你带来太多好处。

那么为什么我们可以从I / O密集型操作中获益呢? 因为我们可以使用I / O完成端口 。 IOCPfunction非常强大,因为在执行整个操作期间,不会消耗服务器上的任何线程或资源。

他们如何工作?

假设我们要使用WebClient.DownloadStringAsync方法下载远程网页的内容。 你调用这个方法将在操作系统中注册一个IOCP并立即返回。 在处理整个请求期间,服务器上不会消耗线程。 一切都在远程服务器上进行。 这可能需要很多时间,但是你不关心,因为你不会危害你的工作线程。 一旦收到响应,就会发出IOCP信号,从线程池中抽取线程,并在该线程上执行callback。 但是正如你所看到的,在整个过程中,我们并没有垄断任何线索。

FileStream.BeginRead,SqlCommand.BeginExecute等方法也是如此。

并行多个数据库调用呢? 假设您有一个同步控制器操作,您按顺序执行了4个阻止数据库调用。 很容易计算,如果每个数据库调用需要200ms,那么您的控制器操作将需要大约800ms执行。

如果您不需要按顺序运行这些调用,那么将其并行化会提高性能?

这是一个很大的问题,不容易回答。 也许是,也许不是。 这完全取决于你如何实现这些数据库调用。 如果您使用前面讨论的asynchronous控制器和I / O完成端口,那么您将提升此控制器操作的性能以及其他操作的性能,因为您不会独占工作线程。

另一方面,如果你实现的不好(在线程池中的一个线程上执行一个阻塞的数据库调用),基本上这个动作的总执行时间会降低到大约200ms,但是你会消耗4个工作线程,所以你可能会降低其他请求的性能,这些请求可能因池中缺less线程来处理它们而变得匮乏。

所以这是非常困难的,如果你没有准备好对你的应用程序进行大量的testing,不要实现asynchronous控制器,因为你可能会做的更多的损害,而不是利益。 只有在有理由的情况下才能实现它们:例如,您已经确定,标准同步控制器操作对您的应用程序来说是一个瓶颈(经过大量的负载testing和测量)。

现在让我们考虑一下你的例子:

 public ViewResult Index() { Task.Factory.StartNew(() => { //Do an advanced looging here which takes a while }); return View(); } 

当接收到索引操作的请求时,将从线程池中提取线程以执行其主体,但其主体仅使用TPL调度新任务。 所以动作执行结束,线程返回到线程池。 除此之外, TPL使用线程池中的线程来执行它们的处理。 所以,即使原始线程返回到线程池,您也从该池中绘制了另一个线程来执行任务的主体。 所以你已经从宝贵的游泳池中破坏了2个线程。

现在让我们考虑以下几点:

 public ViewResult Index() { new Thread(() => { //Do an advanced looging here which takes a while }).Start(); return View(); } 

在这种情况下,我们手动产生一个线程。 在这种情况下,Index操作主体的执行可能会稍微延长(因为产生一个新的线程比从现有的池中抽取一个要花费更多)。 但是高级日志操作的执行将在不属于池的线程上完成。 所以我们不会损害游戏池中的线程,这些线程可以为另一个请求提供服务。

是的 – 所有线程都来自线程池。 你的MVC应用程序已经是multithreading的,当一个新的线程的请求进入时,将从该池中取出并用于处理请求。 该线程将被“locking”(来自其他请求),直到请求被完全服务并完成。 如果池中没有可用线程,则请求将不得不等待,直到有一个线程可用。

如果你有asynchronous控制器,他们仍然从池中获得一个线程,但是在服务请求的时候,他们可以放弃线程,等待一些事情发生(并且该线程可以被赋予另一个请求),当原始请求需要一个线程它再次从池中获得一个。

不同之处在于,如果您有很多长时间运行的请求(线程正在等待某个响应),那么您可能会用尽池中的线程来处理基本请求。 如果您有asynchronous控制器,则不会有更多的线程,但正在等待的线程将返回到池中,并可以处理其他请求。

一个近乎真实的生活的例子…想想就像上公共汽车,有五个人等着上车,第一个上车,付款和坐下(司机服务他们的要求),你(司机正在服务你的要求),但你找不到你的钱; 当你在口袋里摸索的时候,司机会放弃你,然后得到下两个人(服务他们的请求),当你发现你的钱时,司机又开始和你打交道(完成你的请求) – 第五个人必须等到你完成了,但第三和第四个人得到了服务,而你中途获得服务。 这意味着驾驶员是游泳池中的唯一线程,乘客就是要求。 如果有两名车手的话,那么写出如何运作就太复杂了,但你可以想象…

如果没有一个asynchronous控制器,你身后的乘客将不得不等待一段时间,而你找钱的同时,公交车司机也不会工作。

因此,结论是,如果很多人不知道他们的钱在哪里(即需要很长时间来回答司机的要求),asynchronous控制器可以帮助处理请求的吞吐量,从而加快了一些处理的速度。 没有aysnc控制器,每个人都会等待,直到前面的人已经完成处理。 但是别忘了,在MVC中,你在一条总线上有很多的总线驱动程序,所以asynchronous并不是一个自动select。

这里有两个概念。 首先我们可以让我们的代码并行执行,或者在另一个线程上执行代码,以避免让用户等待。 你有的例子

 public ViewResult Index() { Task.Factory.StartNew(() => { //Do an advanced looging here which takes a while }); return View(); } 

属于第二类。 用户将获得更快的响应,但服务器上的总工作量更高,因为它必须执行相同的工作+处理线程。

另一个例子是:

 public ViewResult Index() { Task.Factory.StartNew(() => { //Make async web request to twitter with WebClient.DownloadString() }); Task.Factory.StartNew(() => { //Make async web request to facebook with WebClient.DownloadString() }); //wait for both to be ready and merge the results return View(); } 

因为这些请求并行运行,所以用户不必等待,只要它们在串行完成的地方。 但是,您应该认识到,我们在这里耗费的资源比在串口运行时多,因为我们在multithreading中运行代码,而线程也在等待。

这在客户端场景中是完美的。 在一个新的任务中包含同步长时间运行的代码(在另一个线程上运行)也是很常见的,同样也保持了ui的响应或者平行化,以使其更快。 一个线程仍然在整个持续时间使用。 在高负载的服务器上,这可能会适得其反,因为您实际上使用了更多的资源。 这是人们警告你的

MVC中的asynchronous控制器有另一个目标。 这里的要点是避免让线程无所事事(这可能会影响可伸缩性)。 如果您所调用的API具有asynchronous方法,那真的很重要。 像WebClient.DowloadStringAsync()一样。

重点是你可以让你的线程返回来处理新的请求,直到Web请求完成,它会调用你callback获得相同的或新的线程,并完成请求。

我希望你了解asynchronous和并行的区别。 把并行代码想象成你的线程所在的代码,并等待结果。 虽然asynchronous代码是代码,当代码完成时你会得到通知的代码,你可以回到它的工作,同时线程可以做其他工作。

如果操作asynchronous运行,则应用程序可以更好地扩展,但只有在有资源可用于服务其他操作时才 可以扩展。

asynchronous操作确保您永远不会阻止某个操作,因为现有操作正在进行。 ASP.NET有一个允许多个请求并行执行的asynchronous模型。 将请求排队并处理它们将是可能的,但是如果有数百个请求排队,并且每个请求需要100ms来处理,这将不能很好地扩展。

如果stream量很大,最好不要asynchronous执行查询, 因为可能没有额外的资源来处理请求 。 如果没有备用资源,你的请求将被强制排队,成倍地增长或彻底失败,在这种情况下,asynchronous开销(互斥和上下文切换操作)不会给你任何东西。

就ASP.NET而言,你没有select – 它使用asynchronous模型,因为这对于服务器 – 客户机模型是有意义的。 如果您要在内部编写您自己的代码,并使用asynchronous模式尝试扩展,除非您试图pipe理所有请求之间共享的资源,否则实际上看不到任何改进,因为它们已被包装在一个不会阻塞别的东西的asynchronous过程中。

最终,在你真正看到什么导致了系统瓶颈的情况下,这一切都是主观的。 有时,很明显,asynchronous模式会有帮助(通过阻止排队的资源阻塞)。 最终只有测量和分析一个系统可以指示你可以在哪里获得效率。

编辑:

在你的例子中, Task.Factory.StartNew调用将在.NET线程池上排队一个操作。 线程池线程的性质将被重用(以避免创build/销毁大量线程的成本)。 一旦操作完成,线程被释放回池以供其他请求重新使用(垃圾收集器实际上并不涉及,除非在操作中创build了一些对象,在这种情况下,它们按照正常方式收集作用域)。

就ASP.NET而言,这里没有特别的操作。 ASP.NET请求完成而不考虑asynchronous任务。 唯一的问题可能是你的线程池是否饱和(即现在没有线程可用来请求请求,池的设置不允许创build更多的线程),在这种情况下请求被阻塞, 等待启动任务直到池线程变为可用。

是的,他们使用线程池中的一个线程。 实际上MSDN中有一个相当出色的指南,可以解决你所有的问题和更多的问题。 过去我发现它非常有用。 一探究竟!

http://msdn.microsoft.com/en-us/library/ee728598.aspx

同时,您听到的关于asynchronous代码的意见+build议应该是一丝不苟。 对于初学者来说,只是做一些asynchronous的事情并不一定会使其规模更好,在某些情况下会使应用程序的规模变得更糟。 您发表的关于“大量stream量……”的其他评论在某些情况下也是正确的。 这实际上取决于您的操作在做什么,以及它们如何与系统的其他部分进行交互。

总之,很多人对asynchronous有很多意见,但是他们可能不正确。 我会说专注于你的确切问题,并做基本的性能testing,看看asynchronous控制器等实际处理您的应用程序。

首先它不是MVC,而是维护线程池的IIS。 所以任何来自MVC或ASP.NET应用程序的请求都是由线程池中维护的线程提供的。 只有使应用程序asynchronous,他调用此操作在不同的线程,并立即释放线程,以便其他请求可以采取。

我已经解释了相同的细节video( http://www.youtube.com/watch?v=wvg13n5V0V0/“MVCasynchronous控制器和线程饥饿”),它显示了如何在MVC中发生线程饥饿,以及如何通过使用MVC最小化asynchronous控制器。我也使用perfmon测量了请求队列,以便您可以看到如何减lessMVCasynchronous请求队列以及如何最差的同步操作。;