在 ASP.NET MVC 中使用.NET 4上的 ThreadPool 中的线程执行异步操作

在这个问题之后,它使我在使用异步时感到很舒服 所以,我写了两篇关于这个的博客文章:

对于 ASP.NET MVC 上的异步操作,我有太多的误解。

我经常听到这句话: 如果操作异步运行,应用程序可以更好地伸缩

我也经常听到这样的句子: 如果流量很大,最好不要异步执行查询——为一个请求消耗2个额外线程会占用其他传入请求的资源。

我认为这两个句子不一致。

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

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

例如,当我们实现一个 AsyncController 时,应用程序是如何构造的?如果我得到了巨大的流量,那么实现 AsyncController 是一个好主意吗?

有没有人可以把我眼前的黑色窗帘拿开,给我解释一下 ASP.NET MVC 3(NET 4)中的异步问题?

编辑:

下面的文档我已经读过几百遍了,我理解其中的主要内容,但我仍然感到困惑,因为有太多不一致的评论。

在 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


}
}
103794 次浏览

是的,他们使用线程池中的线程。实际上,MSDN 有一个非常优秀的指南,可以解决你所有的问题,甚至更多。我发现它在过去很有用。看看这个!

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

同时,您听到的关于异步代码的注释 + 建议应该持保留态度。对于初学者来说,仅仅创建异步并不一定会使其扩展得更好,而且在某些情况下可能会使应用程序的扩展性变差。你发表的另一条关于“巨大流量... ...”的评论也只有在某些情况下才是正确的。这实际上取决于您的操作正在做什么,以及它们如何与系统的其他部分进行交互。

简而言之,很多人对异步有很多看法,但脱离上下文可能是不正确的。我认为应该集中精力解决实际的问题,并进行基本的性能测试,以了解什么样的异步控制器可以实际处理 你的应用程序。

如果操作异步运行,应用程序 可以的伸缩性会更好,但是 只有在有可用资源为附加操作提供服务的情况下

异步操作可以确保您永远不会因为现有操作正在进行而阻塞该操作。NET 有一个异步模型,允许多个请求并行执行。将请求排队并处理它们 FIFO 是可能的,但是当您有数百个请求排队并且每个请求需要100毫秒处理时,这种方法就不能很好地扩展。

如果流量很大,那么 最好不要异步执行查询 因为可能没有额外的资源为请求提供服务。如果没有备用资源,请求就会被迫排队,花费指数级的时间或彻底失败,在这种情况下,异步开销(互斥锁和上下文切换操作)不会给您带来任何东西。

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

最终,这都是主观的,直到你真正看到是什么导致了你系统的瓶颈。有时,异步模式(通过防止排队的资源阻塞)在哪些方面有所帮助是显而易见的。最终,只有对系统进行测量和分析,才能说明在哪里可以获得效率。

编辑:

在您的示例中,Task.Factory.StartNew调用将在。NET 线程池。线程池线程的本质是重用(以避免创建/销毁大量线程的成本)。一旦操作完成,线程就被释放回池中,由另一个请求重新使用(除非在操作中创建了一些对象,否则垃圾收集器实际上不会参与进来,在这种情况下,它们将按照正常范围收集)。

就 ASP.NET 而言,这里没有特殊的操作。NET 请求完成时不考虑异步任务。唯一的问题可能是您的线程池是否已经饱和(即现在没有可用的线程来服务请求,而且池的设置不允许创建更多的线程) ,在这种情况下,请求被阻塞 等待着开始这项任务,直到一个池线程变得可用。

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

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

区别在于,如果有许多长时间运行的请求(其中线程正在等待某个响应) ,则可能会用尽池中的线程来服务甚至基本的请求。如果您有异步控制器,那么就不会有更多的线程,但是那些正在等待的线程将返回到池中,并且可以为其他请求提供服务。

一个 差不多的现实生活例子..。 想象一下,有五个人等着上车,第一个人上车,付钱,然后坐下(司机满足了他们的要求) ,你上车(司机满足了你的要求) ,但是你找不到你的钱; 当你在口袋里摸索的时候,司机放弃了你,让接下来的两个人上车(满足他们的要求) ,当你找到你的钱的时候,司机又开始和你打交道(完成你的要求)——第五个人必须等到你完成,但是第三个和第四个人得到了服务,而你已经得到了一半的服务。这意味着驱动程序是池中唯一的线程,乘客是请求。如果有两个驾驶员的话,要写出它是如何工作的就太复杂了,但是你可以想象..。

如果没有一个异步控制器,你身后的乘客将不得不等待很长时间,而你寻找你的钱,同时公共汽车司机将不会做任何工作。

因此,结论是,如果很多人不知道他们的钱在哪里(例如,需要很长时间来响应驱动程序要求的东西) ,异步控制器可以很好地帮助请求的吞吐量,加快一些请求的处理速度。如果没有 aysnc 控制器,每个人都会等待,直到前面的人被完全处理。但是不要忘记,在 MVC 中,在一个总线上有很多总线驱动程序,所以异步不是一个自动选择。

为了更好地理解 ASP.NET 中的异步处理(这就是异步控制器基本上所代表的) ,我建议您阅读下面的 好文章

让我们首先考虑一个标准的同步操作:

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

当对此操作发出请求时,将从线程池中抽取一个线程,并在此线程上执行此操作的主体。因此,如果这个操作中的处理速度很慢,那么整个处理过程都会阻塞这个线程,因此这个线程不能被重用来处理其他请求。在请求执行结束时,线程被返回到线程池。

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

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


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

当向 Index 操作发送请求时,将从线程池中抽取一个线程,并执行 IndexAsync方法的主体。一旦此方法的主体执行完毕,线程将返回到线程池。然后,使用标准的 AsyncManager.OutstandingOperations,一旦发出异步操作完成的信号,就从线程池中抽取另一个线程,并在该线程上执行 IndexCompleted动作的主体,并将结果呈现给客户机。

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

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

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

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

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

它们是如何工作的?

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

对于 FileStream.BeginRead、 SqlCommand.BeginExecute、 ... 等方法也是如此。

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

如果不需要顺序运行这些调用,并行化它们会提高性能吗?

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

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

因此,这是非常困难的,如果您觉得还没有准备好在应用程序上执行大量的测试,那么不要实现异步控制器,因为这样做可能弊大于利。只有在您有理由这样做时才实现它们: 例如,您已经确定标准的同步控制器操作是应用程序的瓶颈(当然是在执行大量负载测试和度量之后)。

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

public ViewResult Index() {


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


return View();
}

当接收到对 Index 操作的请求时,将从线程池中抽取一个线程来执行其主体,但其主体仅使用 TPL调度新任务。因此,操作执行结束,线程返回到线程池。除此之外,TPL使用线程池中的线程来执行它们的处理。因此,即使原始线程被返回到线程池,您也已经从该池中绘制了另一个线程来执行任务体。所以你已经危害了你宝贵的线程池中的两个线程。

现在让我们考虑以下情况:

public ViewResult Index() {


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


return View();
}

在这种情况下,我们手动生成一个线程。在这种情况下,Index 操作主体的执行可能需要稍长的时间(因为生成一个新线程比从现有池中绘制一个线程更昂贵)。但是高级日志记录操作的执行将在不属于池的线程上完成。因此,我们不会危害来自池的线程,这些线程仍然可以为其他请求提供空闲服务。

这里有两个概念在起作用。首先,我们可以让代码并行运行,以加快执行速度,或者在另一个线程上调度代码,以避免让用户等待。你的榜样

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 请求完成,在那里它会调用你的回调,得到相同的或一个新的线程,并完成请求。

我希望您能够理解异步和并行之间的区别。可以将并行代码看作是线程围坐在一起等待结果的代码。虽然异步代码是代码,当代码完成时会通知您,您可以重新开始工作,同时线程可以执行其他工作。

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

我在一个详细的视频(http://www.youtube.com/watch?v=wvg13n5V0V0/“ MVC 异步控制器和线程饥饿”)中也做了同样的解释,这个视频展示了在 MVC 中线程饥饿是如何发生的,以及如何通过使用 MVC 异步控制器来最小化线程饥饿。我还使用 Perfmon 测量了请求队列,这样你就可以看到 MVC 异步请求队列是如何减少的,同步操作的情况又是如何最糟糕的。