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


158

提出这个问题后,在ASP.NET MVC中使用异步操作时,我感到很舒服。因此,我就此写了两篇博客文章:

我对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

    }
}

不确定答案,但值得注意的是异步和多线程是不同的东西。因此,有可能具有固定数量的异步处理线程。当一个页面不得不阻塞说,例如I / O时,另一页面将有机会在同一线程上运行。这两个语句都是正确的,异步可以使事情更快,但是线程太多是个问题。
克里斯·奇尔弗斯

@ChrisChilvers是的,异步操作不一定总是需要多线程。我已经知道了,但是据我所知,我对此没有控制权。从我的角度来看,AsyncController加速了它想要多少个线程,但也不确定。在WPF等桌面应用程序上也有线程池的概念吗?我认为在这类应用程序上,线程数不是问题。
tugberk 2012年


我认为问题(因此不一致)是第二条语句在很多线程时使用异步。这可能是因为asp.net已实现了异步页面,因此特定的实现使该问题感到困惑(因为引起问题的功能的名称将是异步页面),但是我不确定具体的含义。因此,它们的意思是“许多线程”,或者它们的意思是“ asp.net X版中的异步页面”,因为将来的版本可能会更改实现。或者,它们只是意味着使用线程池在页面内执行异步。
克里斯·奇弗斯

@ChrisChilvers哦,伙计!经过这些评论,我感到更加困惑:s
tugberk 2012年

Answers:


177

这是一篇非常不错的文章,我建议您阅读以更好地理解ASP.NET中的异步处理(异步控制器基本上代表了异步处理)。

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

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

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

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

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

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

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

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

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

那么什么时候我们可以真正利用异步控制器呢?

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

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

它们如何运作?

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

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

如何并行化多个数据库调用?假设您有一个同步控制器操作,其中您依次执行了4个阻塞数据库调用。可以很容易地计算出,如果每个数据库调用花费200毫秒,则您的控制器动作将花费大约800毫秒来执行。

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

这是个大问题,很难回答。可能是,可能不是。这完全取决于您如何实现这些数据库调用。如果您如前所述使用异步控制器和I / O完成端口,则将提高此控制器操作和其他操作的性能,因为您不会垄断工作线程。

另一方面,如果执行效果不佳(对线程池中的线程执行阻塞数据库调用),则基本上可以将该操作的总执行时间降低至大约200ms,但是您将消耗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
真的很详细,谢谢!假设我们System.Threading.TaskIndexAsync方法内部运行了4个异步Tasks()。在这些操作中,我们正在对服务器进行数据库调用。因此,所有这些都是I / O密集型操作,对吗?在那种情况下,我们是否创建4个单独的线程(或从线程池中获得4个单独的线程)?假设我有一台多核计算机,它们也将并行运行,对吗?
tugberk 2012年

10
@tugberk,数据库调用是I / O操作,但这全取决于您如何实现它们。如果您使用阻塞数据库调用(例如,SqlCommand.ExecuteReader您正在浪费一切,因为这是阻塞调用)。您正在阻止执行此调用的线程,如果该线程恰好是池中的线程,那就非常糟糕。仅当您使用I / O完成端口:时,您才会受益SqlCommand.BeginExecuteReader。如果不管您做什么都不使用IOCP,请不要使用异步控制器,因为这样做会造成更大的损害,而不是对应用程序的整体性能有所帮助。
Darin Dimitrov

1
好吧,大多数时候我会先使用EF代码。我不确定是否合适。我提供了一个样本,该样本显示了我的一般操作。我更新了问题,可以看看吗?
tugberk 2012年

3
@tugberk,您正在并行运行它们,因此与顺序运行它们相比,执行的总时间要少。但是为了运行它们,您需要使用工作线程。好吧,实际上EF是懒惰的,所以当您这样做_context.Foo时,实际上并没有执行任何操作。您只是在构建一个表达式树。对此要格外小心。仅当您开始对结果集进行枚举时,才推迟查询执行。而且如果这种情况发生在视图中,可能会对性能造成灾难性的影响。要急于执行EF查询.ToList(),请在末尾附加。
Darin Dimitrov

2
@tugberk,您将需要一个负载测试工具,以便在您的网站上并行模拟多个用户,以查看它在重负载下的行为。Mini Profiler无法模拟您网站上的负载。它可能有助于您查看和优化ADO.NET查询并配置单个请求,当您需要查看站点在许多用户点击时在现实世界中的行为时,该请求毫无用处。
Darin Dimitrov 2012年

49

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

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

区别在于,如果您有许多长时间运行的请求(线程正在等待某个内容的响应),则池中的线程可能用完了,甚至无法满足基本请求。如果您具有异步控制器,那么您将没有更多线程,但是正在等待的那些线程将返回到池中并可以处理其他请求。

一个几乎现实的例子...想起来就像在公共汽车上,有五个人在等着上车,第一个上车,付钱并坐下来(驾驶员为他们的要求提供服务),然后您上车(驾驶员正在维修)您的请求),但您找不到钱;当您在口袋里摸索时,驾驶员放弃了您,接下了另外两个人(为他们服务),当您发现您的钱时,驾驶员再次与您打交道(完成您的请求)-第五个人必须等到您已经完成,但是第三和第四个人在您服役到一半时得到了服务。这意味着驾驶员是游泳池中唯一的线程,乘客是请求者。如果有两个驱动程序,写它如何工作太复杂了,但是您可以想象...

如果没有异步控制器,您后面的乘客将在等待您寻找钱的同时等待年龄,而公交车司机将不做任何工作。

因此得出的结论是,如果很多人不知道自己的钱在哪里(即需要很长时间才能响应驾驶员提出的要求),异步控制器可以很好地帮助提高请求的吞吐量,从而加快某些过程的速度。没有aysnc控制器,每个人都会等到前面的人被完全处理。但是不要忘记,在MVC中,单个总线上有很多总线驱动程序,因此异步并不是自动选择。


8
很好的类比。谢谢。
匹兹堡DBA 2012年

我喜欢这个描述。谢谢
Omer Cansizoglu 2012年

解释它的绝佳方法。谢谢您
Anand Vyas 2015年

您的答案与Darin的答案相结合,总结了异步控制器背后的整个机制,它是什么,更重要的是它不是!
尼尔曼

这是一个很好的类比,但我唯一的问题是:这个类比的家伙摸索着他的口袋将是我们应用程序中的某种工作/处理……那么,一旦我们释放线程,哪个进程会起作用?难道这是另一个线程?那我们在这里能得到什么呢?
Tomuke

10

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

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请求完成为止,它将在此调用您的回调,该回调将获取相同或新线程并完成请求。

希望您了解异步与并行之间的区别。将并行代码视为线程位于其中并等待结果的代码。尽管异步代码是代码,代码完成后会通知您,您可以重新使用它,与此同时,线程可以执行其他工作。


6

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

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

如果你有交通量巨大,你可能会更好不是异步执行的查询,因为可能没有额外的资源来服务请求。如果没有多余的资源,您的请求将被迫排队,经历成倍的时间延长或完全失败,在这种情况下,异步开销(互斥量和上下文切换操作)无法为您提供任何帮助。

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

最终,这一切都是主观的,直到您真正了解造成系统瓶颈的原因。有时候,很明显异步模式会有所帮助(通过防止排队的资源阻塞)。最终,只有对系统进行测量和分析才能表明您可以在何处获得效率。

编辑:

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

就ASP.NET而言,这里没有特殊的操作。在不考虑异步任务的情况下完成ASP.NET请求。唯一需要担心的是,如果您的线程池已饱和(即,现在没有线程可用于为请求提供服务,并且该池的设置不允许创建更多线程),在这种情况下,请求被阻止,等待启动任务直到池线程变得可用。


谢谢!阅读您的答案后,我使用代码示例编辑了问题。你可以看看吗?
tugberk 2012年

您在这里有个神奇的句子:Task.Factory.StartNew调用将使.NET线程池上的操作排队。。在这种情况下,这里是正确的:1-)它创建了一个新线程,完成后,该线程返回线程池并等待在那里再次使用。2-)它从线程池获取一个线程,然后该线程返回线程池并等待在那里再次使用。3-)它采用最有效的方法,并且可以执行任何一种。
tugberk 2012年

1
线程池根据需要创建线程,并在不使用线程时回收线程。在CLR版本之间,它的确切行为有所不同。您可以在此处找到有关它的特定信息msdn.microsoft.com/en-us/library/0ka9477y.aspx
Paul Turner

现在,它开始在我的脑海中成形。因此,CLR拥有线程池,对吗?例如,WPF应用程序还具有线程池的概念,并且也处理线程池。
tugberk 2012年

1
线程池是CLR中自己的东西。“知道”该池的其他组件表明它们在适当的地方使用了线程池线程,而不是创建和销毁自己的线程。创建或销毁线程是一个相对昂贵的操作,因此使用池可在短期运行的操作中获得很大的效率提升。
保罗·特纳

2

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

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

同时,对异步代码的评论和建议应该引起注意。对于初学者而言,仅使异步执行不一定会使其伸缩性更好,并且在某些情况下会使您的应用程序伸缩性更差。您发布的有关“大量流量...”的其他评论也仅在某些情况下是正确的。这实际上取决于您的操作在做什么,以及它们如何与系统的其他部分进行交互。

简而言之,很多人对异步有很多看法,但是在上下文之外它们可能是不正确的。我想说的是专注于您的确切问题,并进行基本的性能测试,以查看异步控制器等在您的应用程序中实际处理了什么。


我可能已经阅读了数百次该文档,但仍然感到非常困惑(也许问题出在我身上,谁知道)。当您环顾四周时,您会在我的问题中看到太多关于ASP.NET MVC异步的不一致评论。
tugberk 2012年

最后一句话:在控制器动作中,我分别查询了5次数据库(我必须这样做),而这总共花费了大约400毫秒。然后,我实现了AsyncController并并行运行它们。响应时间大大减少到大约 200毫秒 但是我不知道它会创建多少个线程,用完这些线程后会发生什么情况,在完成GC后立即清理它们,以使我的应用程序不会出现内存泄漏,依此类推。这方面的任何想法。
tugberk 2012年

附加调试器并查找。
2012年

0

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

我已经通过详细视频(http://www.youtube.com/watch?v=wvg13n5V0V0/ “ MVC异步控制器和线程饥饿”)对此进行了解释,该视频演示了MVC中线程饥饿的发生情况以及如何通过使用MVC来将线程饥饿最小化异步控制器。我还使用perfmon测量了请求队列,以便您可以了解如何减少MVC异步的请求队列以及对Synch操作的不利影响。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.