2012-01-05 70 views
148

在这个问题后,在ASP.NET MVC中使用异步 操作时,我感到很舒服。所以,我写了两个博客文章:在ASP.NET MVC中使用ThreadPool中的异步操作4

我有我的心意,想在ASP.NET MVC的异步操作太多的误解。

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

而且听说这种句子很多,以及:如果你有交通量巨大,你可能最好不要异步执行查询 - 消耗2个额外的线程来处理一个请求,这会将资源从其他传入的请求中分离出来。

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

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

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

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

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

编辑:

我已阅读本文件下方近数百次,我理解主要交易,但我仍然有困惑,因为有太多的不一致意见那里。

Using an Asynchronous Controller in ASP.NET MVC

编辑:

假设我有一个像下面的控制器动作(不是AsyncController的实现虽然):

public ViewResult Index() { 

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

    return View(); 
} 

正如你看到这里,我火的操作并忘记它。然后,我立即返回而无需等待它完成。

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

编辑:

对于@ Darin的答案,这里是异步代码会谈到数据库中的样本:

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 

    } 
} 
+0

不知道答案,但其值得注意的异步和多线程是不同的东西。所以有可能拥有固定数量的异步处理线程。会发生什么情况是当一个页面必须阻止I/O时,另一个页面才有机会在同一个线程上运行。这两种语句如何可以是真实的,异步可以使事情变得更快,但太多的线程是一个问题。 – 2012-01-05 13:23:50

+0

@ChrisChilvers是的,在异步操作中,多线程并不总是必需的。据我所知,我已经想到了,但我认为我没有任何控制者。 AsyncController从我的角度旋转它想要的线程数量,但不确定。在WPF等桌面应用程序中是否还有线程池的概念?我认为线程的数量不是这些类型的应用程序的问题。 – tugberk 2012-01-05 13:28:25

+5

看看视频[杰夫里氏超线程(http://channel9.msdn.com/Shows/AppFabric-tv/AppFabrictv-Threading-with-Jeff-Richter) – oleksii 2012-01-05 13:32:38

回答

159

这里是excellent article我建议您阅读以更好地理解ASP.NET中的异步处理(这是异步控制器基本表示的内容)。

我们先考虑一个标准的同步动作:

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

当一个请求到这个行动线程从线程池中提取这个动作的机构提出的在此线程执行。因此,如果此操作内的处理速度较慢,则会阻止此线程进行整个处理,因此此线程无法重用来处理其他请求。在请求执行结束时,线程返回到线程池。

现在,让我们在异步模式的一个例子:

public void IndexAsync() 
{ 
    // perform some processing 
} 

public ActionResult IndexCompleted(object result) 
{ 
    return View(); 
} 

当一个请求被发送到索引操作,一个线程从线程池中提取并执行IndexAsync方法的主体。一旦该方法的主体完成执行,该线程就返回到线程池。然后,使用标准AsyncManager.OutstandingOperations,一旦您发出完成异步操作的信号,将从线程池中抽取另一个线程,并在其上执行IndexCompleted操作的正文并将结果呈现给客户端。

所以我们可以看到在这种模式下,单个客户端HTTP请求可以由两个不同的线程执行。

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

那么,我们什么时候才能真正发挥异步控制器的优势?

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

那么,为什么我们能从I/O密集型操作中获益呢?因为我们可以使用I/O Completion Ports。 IOCP功能非常强大,因为在执行整个操作期间,您不会占用服务器上的任何线程或资源。

它们是如何工作的?

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

同样的立场与方法,如FileStream.BeginRead,SqlCommand.BeginExecute,真的......

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

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

这是一个很大的问题,不容易回答。也许是,也许不是。它完全取决于你如何实现这些数据库调用。如果您像前面讨论的那样使用异步控制器和I/O完成端口,那么您将提升此控制器操作的性能以及其他操作的性能,因为您不会独占工作线程。另一方面,如果你实现它们的效果不佳(在线程池中的线程上执行阻塞数据库调用),基本上可以将执行此操作的总时间降低到大约200毫秒,但是你会消耗4个工作线程,因此您可能会降低其他请求的性能,这些请求可能因池中缺少线程来处理它们而变得匮乏。

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

现在让我们考虑您的示例:

public ViewResult Index() { 

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

    return View(); 
} 

当接收到Index操作线程被从线程池中提取执行其身体的要求,但其机身仅安排一个新的任务使用TPL。所以动作执行结束并且线程返回到线程池。除此之外,TPL使用线程池中的线程来执行它们的处理。因此,即使原始线程返回到线程池,您也从该池抽取了另一个线程来执行任务的主体。所以你已经危害了宝贵游泳池中的2条线。

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

public ViewResult Index() { 

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

    return View(); 
} 

在这种情况下,我们人工产卵一个线程。在这种情况下,Index操作主体的执行可能会稍微延长(因为产生一个新线程比从现有池中抽取更昂贵)。但是高级日志记录操作的执行将在不属于池的线程上完成。所以我们不会损害游泳池中的线索,这些线索可以免费为另一个请求服务。

+1

真的很详细,谢谢!假设我们有4个异步任务('System.Threading.Task')在'IndexAsync'方法内运行。在这些操作中,我们正在对服务器进行数据库调用。所以,他们都是I/O密集型操作,对吧?在这种情况下,我们是否创建4个独立的线程(或从线程池获取4个独立的线程)?假设我有一台多核机器,它们也将并行运行,对吧? – tugberk 2012-01-05 14:41:21

+10

@tugberk,数据库调用是I/O操作,但它将全部取决于你如何实现它们。如果你使用阻塞数据库调用,比如'SqlCommand.ExecuteReader',你就是在浪费一切,因为这是一个阻塞调用。您正在阻止执行此调用的线程,并且如果此线程恰好是池中的线程,那么它非常糟糕。只有使用I/O完成端口时才会受益:'SqlCommand.BeginExecuteReader'。如果不使用IOCP,无论您做什么,都不要使用异步控制器,因为您将受到的损害比应用程序的整体性能受益更大。 – 2012-01-05 14:50:34

+1

那么,大多数时候我首先使用EF代码。我不确定它是否合适。我举了一个样本,显示了我通常所做的事情。我更新了这个问题,你可以看看吗? – tugberk 2012-01-05 15:14:10

2

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

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

同时,你听到有关异步代码中的注释+建议应采取与一粒盐。对于初学者来说,仅仅做一些异步并不一定会使它更好地扩展,并且在某些情况下可能会让应用程序变得更糟。您发布的关于“大量流量......”的其他评论在某些情况下也是正确的。这实际上取决于您的操作在做什么,以及它们如何与系统的其他部分进行交互。

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

+0

我已经阅读过这个文档可能有好几百次,但我仍然有很多困惑(也许问题在于我,谁知道)。当你环顾四周时,你会看到关于ASP.NET MVC异步的非常多不一致的评论,就像你在我的问题上看到的那样。最后一句为 – tugberk 2012-01-05 13:20:20

+0

:在一个控制器动作中,我单独查询数据库5次(我不得不),并且这大约需要400毫秒。然后,我实现了AsyncController并行运行它们。响应时间大幅缩短至约。 200毫秒。但是我不知道它创建了多少线程,在完成这些线程之后会发生什么,GC'是否会在完成后立即清理,以便我的应用程序不会泄漏内存,所以等等。这部分的任何想法。 – tugberk 2012-01-05 14:05:26

+0

附加一个调试器并找出答案。 – 2012-01-05 15:20:25

6

应用可以的,如果操作异步运行仅当有可用于服务额外的操作资源变得更好,但

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

如果你有交通量巨大,你可能最好不异步执行的查询,如可能没有额外的资源来服务请求。如果没有备用资源,你的请求将被强制排队,延长或彻底失败,在这种情况下,异步开销(互斥锁和上下文切换操作)不会给你任何东西。

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

最终,除非您真正了解导致系统瓶颈的因素,否则它们都是主观的。有时很明显,异步模式会有帮助(通过阻止排队的资源阻塞)。最终只有测量和分析一个系统才能指出你可以在哪里获得效率。

编辑:

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

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

+0

谢谢!在我阅读您的答案后,我使用代码示例编辑了该问题。你能看看吗? – tugberk 2012-01-05 13:55:44

+0

希望我的更新能够解答您的其他问题。 – 2012-01-05 14:03:44

+0

您对我有一个神奇的句子:**'Task.Factory.StartNew'调用将在.NET线程池中排队操作。**。在这种情况下,哪一个在这里是正确的:** 1 - )**它创建一个新的线程,当它完成时,该线程返回到线程池并等待在那里再次被重用。 ** 2 - )**它从线程池中获得一个线程,并且该线程返回到线程池并在那里等待再次使用。 ** 3 - )**它采取最有效的方法,可以做任何一个。 – tugberk 2012-01-05 14:10:30

42

是 - 所有线程都来自线程池。您的MVC应用程序已经是多线程的,当请求进入新线程时,将从该池取出并用于为请求提供服务。该线程将被锁定(来自其他请求),直到请求被完全服务并完成。如果池中没有可用线程,则请求将不得不等待,直到有可用线程可用。

如果你有异步控制器,他们仍然可以从池中的线程,但在维修的要求,他们可以放弃线程在等待一些事情发生(以及线程可以给另一个请求),并在原始请求再次需要一个线程,它从池中获取一个线程。

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

一个现实生活中的例子... 认为它像得到一个公共汽车上,有五个人等待下去了,第一个得到的,支付和坐下(司机提供服务的请求),你上车(司机正在为你的请求提供服务),但你找不到你的钱;当你在口袋里摸索时,司机会放弃你,并让下两个人(服务他们的请求),当你发现你的钱司机再次与你打交道(完成你的请求) - 第五人必须等到你已经完成,但第三和第四个人得到了服务,而你是中途获得服务。这意味着驾驶员是游泳池中的唯一线程,乘客是请求。如果有两名车手,但如果有两名车手可以想象,它的工作过于复杂,但你可以想象......

没有异步控制器,在你寻找你的钱的同时,你身后的乘客将不得不等待几年,司机会没有工作。因此,结论是,如果很多人不知道他们的钱在哪里(即需要很长时间来响应司机要求的东西),异步控制器可以帮助处理请求的吞吐量,从而加速一些。如果没有aysnc控制器,每个人都会等待,直到前面的人已经完成处理。但是不要忘记,在MVC中,你在一条总线上有很多总线驱动程序,所以异步不是自动选择。

+6

非常好的比喻。谢谢。 – 2012-09-23 04:23:35

+0

我喜欢这个描述。谢谢 – 2012-10-31 18:48:15

+0

很好的解释方法。谢谢, – 2015-02-05 09:47:17

9

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

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(); 
} 

由于并行用户运行的请求不会有,只要他们是否在串行哪里做等待。但是你应该认识到,我们在这里使用的资源比在串口下运行更多,因为我们在多线程中运行代码,而我们也在等待线程。

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

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

的一点是,你可以让你的线程返回,直到web请求完成后它会打电话给你回调它获取相同或一个新的线程,并完成该请求来处理新的请求。

我希望你明白异步和并行之间的区别。把并行代码想象成代码,你的线程坐在那里等待结果。虽然异步代码是代码,当代码完成时您会收到通知,并且您可以重新开始工作,同时该线程可以完成其他工作。

0

首先它不是MVC,而是维护线程池的IIS。因此,任何涉及MVC或ASP.NET应用程序的请求都由线程池中维护的线程提供。只有在制作应用程序Asynch时,他才会在不同的线程中调用此操作,并立即释放线程,以便可以执行其他请求。

我已经解释的相同与细节视频(http://www.youtube.com/watch?v=wvg13n5V0V0/“MVC非同步控制器和线程饥饿”)表示线程starvation如何发生在MVC和其如何通过使用非同步MVC也controllers.I已经测量使用请求队列最小化perfmon,以便您可以看到MVC异步请求队列的减少情况以及对于同步操作的最坏情况。