关于c#:ASP.NET MVC中的异步操作是否使用.NET 4上的ThreadPool中的线程

Do asynchronous operations in ASP.NET MVC use a thread from ThreadPool on .NET 4

After this question, it makes me comfortable when using async
operations in ASP.NET MVC. So, I wrote two blog posts on that:

  • My Take on Task-based Asynchronous Programming in C# 5.0 and ASP.NET MVC Web Applications
  • Asynchronous Database Calls With Task-based Asynchronous Programming Model (TAP) in ASP.NET MVC 4

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

我经常听到这样一句话:如果操作异步运行,应用程序可以更好地扩展。

我也听过很多这样的句子:如果你有大量的流量,你最好不要异步地执行你的查询——用两个额外的线程来服务一个请求会从其他传入的请求中带走资源。

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

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

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

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

有没有人能在我眼前摘下这张黑色的窗帘,向我解释有关ASP.NET MVC 3(NET 4)异步的问题?

编辑:

我已经阅读了近百次以下的文件,我了解主要的交易,但我仍然感到困惑,因为有太多不一致的意见。

在ASP.NET MVC中使用异步控制器

编辑:

假设我有如下的控制器操作(但不是AsyncController的实现):

1
2
3
4
5
6
7
8
public ViewResult Index() {

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

    return View();
}

正如你在这里看到的,我启动了一个手术,忘记了它。然后,我立即返回,不等待它完成。

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

编辑:

对于@darin的答案,下面是一个异步代码示例,它与数据库进行对话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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

    }
}


这是一篇很好的文章,我建议您阅读以更好地理解ASP.NET中的异步处理(异步控制器基本上就是这样表示的)。好的。

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

1
2
3
4
5
public ActionResult Index()
{
    // some processing
    return View();
}

当对此操作发出请求时,将从线程池中提取线程,并在此线程上执行此操作的主体。因此,如果此操作内部的处理速度很慢,则会在整个处理过程中阻塞此线程,因此无法重用此线程来处理其他请求。在请求执行结束时,线程将返回到线程池。好的。

现在让我们以异步模式为例:好的。

1
2
3
4
5
6
7
8
9
public void IndexAsync()
{
    // perform some processing
}

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

当向索引操作发送请求时,将从线程池中提取一个线程,并执行IndexAsync方法的主体。一旦这个方法的主体完成执行,线程就返回到线程池。然后,使用标准的AsyncManager.OutstandingOperations,一旦发出异步操作完成的信号,就会从线程池中提取另一个线程,并对其执行IndexCompleted操作的主体,并将结果呈现给客户机。好的。

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

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

那么我们什么时候才能真正利用异步控制器呢?好的。

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

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

它们是如何工作的?好的。

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

对于filestream.beginread、sqlcommand.beginecute等方法也是如此……好的。

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

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

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

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

因此,这是非常困难的,如果您还没有准备好对应用程序执行广泛的测试,就不要实现异步控制器,因为很可能会造成更多的损害而不是好处。只有在有理由这样做的情况下才能实现它们:例如,您已经确定标准同步控制器操作是应用程序的瓶颈(当然,在执行了大量的负载测试和测量之后)。好的。

现在让我们考虑一下您的示例:好的。

1
2
3
4
5
6
7
8
public ViewResult Index() {

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

    return View();
}

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

现在让我们考虑一下:好的。

1
2
3
4
5
6
7
8
public ViewResult Index() {

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

    return View();
}

在这种情况下,我们将手动生成一个线程。在这种情况下,执行索引操作的主体可能需要稍长的时间(因为生成新线程比从现有池中提取线程更昂贵)。但高级日志记录操作将在不属于池的线程上执行。因此,我们不会危及池中的线程,这些线程可以自由地为其他请求提供服务。好的。好啊。


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

如果您有异步控制器,它们仍然可以从池中获取线程,但是在服务请求时,它们可以放弃线程,而在等待发生某些事情时(该线程可以提供给另一个请求),并且当原始请求再次需要线程时,它会从池中获取一个线程。

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

一个几乎真实的例子…把它想象成上车,有五个人等着上车,第一个上车,付钱坐下(司机按他们的要求做了),你上车(司机按你的要求做了),但你找不到钱;当你在口袋里摸索时,司机放弃了你,让接下来的两个人上车(按他们的要求做),然后当你找到你的钱,司机又开始处理你(完成你的要求)-第五个人必须等到你完成,但第三和第四个人得到了服务,而你正在半路得到服务。这意味着驱动程序是池中唯一的线程,而乘客是请求。如果有两个司机的话,写下它的工作原理太复杂了,但你可以想象…

如果没有异步控制器,你后面的乘客就必须等上好几年才能找到你的钱,同时公共汽车司机也不会工作。

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


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

1
2
3
4
5
6
7
8
public ViewResult Index() {

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

    return View();
}

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

另一个例子是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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请求完成,在那里它将调用回调,回调将获得相同或新线程并完成请求。

我希望您理解异步和并行的区别。把并行代码想象成线程所在的代码,并等待结果。异步代码是代码,当代码完成后,您将收到通知,您可以重新使用它,同时线程可以执行其他工作。


如果操作异步运行,应用程序可以更好地扩展,但前提是有可用的资源来服务其他操作。

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

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

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

归根结底,这都是主观的,直到你真正看到是什么导致了你系统的瓶颈。有时,异步模式在何处有帮助是显而易见的(通过防止排队的资源阻塞)。最终,只有测量和分析一个系统,才能表明在哪里可以获得效率。

编辑:

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

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


是的,它们使用线程池中的线程。事实上,有一个相当优秀的指导,从msdn将解决所有的问题和更多。我发现它在过去非常有用。过来看!

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

同时,您听到的关于异步代码的评论+建议应该一针见血。对于初学者来说,仅仅使某个东西异步并不一定能使它更好地伸缩,而且在某些情况下可能会使应用程序的伸缩性更差。你发表的另一条关于"巨大流量…"的评论也只在某些情况下是正确的。它实际上取决于您的操作在做什么,以及它们如何与系统的其他部分交互。

简言之,很多人对异步有很多看法,但是他们可能在上下文之外是不正确的。我要说的是,集中精力解决您的具体问题,并进行基本的性能测试,以了解异步控制器等对您的应用程序实际处理的是什么。


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

我用一个详细的视频(http://www.youtube.com/watch)也解释了这一点。v=wvg13n5v0v0/"MVC异步控制器和线程不足"),它显示了MVC中如何发生线程不足,以及如何使用MVC异步控制器最小化线程不足。我还使用perfmon测量了请求队列,以便您可以看到MVC异步的请求队列是如何减少的,以及同步操作的最坏情况。