在高流量场景中使用ASP.NET中的ThreadPool.QueueUserWorkItem

时间:2009-08-25 02:11:30

标签: asp.net multithreading threadpool

我一直认为使用ThreadPool(假设非关键)短期后台任务被认为是最佳实践,即使在ASP.NET中也是如此,但后来我遇到了this article似乎另有建议 - 争论的焦点是你应该让ThreadPool处理与ASP.NET相关的请求。

所以这就是我到目前为止一直在做的小异步任务:

ThreadPool.QueueUserWorkItem(s => PostLog(logEvent))

the article建议明确创建一个线程,类似于:

new Thread(() => PostLog(logEvent)){ IsBackground = true }.Start()

第一种方法具有管理和限制的优点,但是有可能(如果文章是正确的)后台任务正在争夺具有ASP.NET请求处理程序的线程。第二种方法释放了ThreadPool,但代价是无限制,因此可能耗尽太多资源。

所以我的问题是,文章中的建议是否正确?

如果你的网站流量太大而你的ThreadPool已经满了,那么最好是带外,还是一个完整的ThreadPool意味着你无论如何都要达到你的资源限制,你不应该尝试创建自己的线程?

澄清:我只是询问小型非关键异步任务(例如远程日志记录)的范围,而不是需要单独过程的昂贵工作项(在这些情况下我同意你需要更强大的溶液)。

11 个答案:

答案 0 :(得分:103)

这里的其他答案似乎遗漏了最重要的一点:

除非您尝试并行化CPU密集型操作以便在低负载站点上更快地完成,否则根本不需要使用工作线程。

这适用于由new Thread(...)创建的免费线程和ThreadPool中响应QueueUserWorkItem请求的工作线程。

是的,确实,你可以通过排队太多工作项来使ThreadPool在ASP.NET进程中挨饿。它将阻止ASP.NET处理进一步的请求。在这方面,文章中的信息是准确的;用于QueueUserWorkItem的相同线程池也用于处理请求。

但是如果你实际上排队了足够的工作项来导致这种饥饿,那么你应该在线程池中挨饿!如果您同时运行数百个CPU密集型操作,那么当计算机已经过载时,让另一个工作线程为ASP.NET请求提供服务会有什么用呢?如果你遇到这种情况,你需要完全重新设计!

大多数时候,我看到或听说过多线程代码在ASP.NET中被不适当地使用,它不是用于排队CPU密集型工作。它用于排队I / O绑定的工作。 如果你想进行I / O工作,那么你应该使用I / O线程(I / O完成端口)。

具体来说,您应该使用您正在使用的任何库类支持的异步回调。这些方法总是非常清晰;他们以BeginEnd开头。与Stream.BeginReadSocket.BeginConnectWebRequest.BeginGetResponse等一样。

这些方法使用ThreadPool,但他们使用IOCP,干扰ASP.NET请求。它们是一种特殊的轻量级线程,可以通过I / O系统的中断信号“唤醒”。在ASP.NET应用程序中,通常每个工作线程都有一个I / O线程,因此每个请求都可以排队一个异步操作。这实际上是数百个异步操作,没有任何显着的性能下降(假设I / O子系统可以跟上)。这比你需要的还要多。

请记住,异步委托不会以这种方式工作 - 它们最终会使用工作线程,就像ThreadPool.QueueUserWorkItem一样。它只是.NET Framework库类的内置异步方法才能够做到这一点。你可以自己做,但它很复杂,有点危险,可能超出了本讨论的范围。

在我看来,这个问题的最佳答案是不要在ASP.NET中使用ThreadPool 背景Thread实例。它根本不像在Windows窗体应用程序中启动一个线程,在那里你可以保持UI响应,而不关心它的效率。在ASP.NET中,您关心的是吞吐量,并且无论您使用ThreadPool,所有这些工作线程上的所有上下文切换都绝对会 kill 您的吞吐量或不。

如果您发现自己在ASP.NET中编写线程代码,请考虑是否可以使用预先存在的异步方法重写它,如果不能,请考虑您是否真的,真的需要代码才能在后台线程中运行。在大多数情况下,您可能会增加复杂性而无需净利益。

答案 1 :(得分:45)

Microsoft的ASP.NET团队的Per Thomas Marquadt,使用ASP.NET ThreadPool(QueueUserWorkItem)是安全的。

From the article

  

Q)如果我的ASP.NET应用程序使用CLR ThreadPool线程,我不会饿死ASP.NET,它也使用CLR ThreadPool来执行请求吗?   块引用

     

A)[T] o总结,不用担心   使线程的ASP.NET饥饿,如果   你认为这里有一个问题   我知道,我们会照顾它。

     

Q)我应该创建自己的线程吗?   (新线程)?这不会更好   对于ASP.NET,因为它使用CLR   线程池。

     

A)请不要。或者说它   不同的方式,不!如果你真的   聪明 - 比我聪明 - 那么你   可以创建自己的线程;   否则,甚至不要考虑它。   以下是您应该采取的一些理由   不经常创建新线程:

     

1)与之相比,它非常昂贵   QueueUserWorkItem ......顺便说一句,如果你能写一个比CLR更好的ThreadPool,我鼓励你申请微软的工作,因为我们肯定在寻找像你这样的人!

答案 2 :(得分:4)

网站不应该绕过产生线程。

您通常会将此功能移至您随后与之通信的Windows服务中(我使用MSMQ与他们交谈)。

- 编辑

我在这里描述了一个实现:Queue-Based Background Processing in ASP.NET MVC Web Application

- 编辑

扩展为什么这比线程更好:

使用MSMQ,您可以与另一台服务器通信。您可以跨机器写入队列,因此如果由于某种原因确定您的后台任务过多地耗尽了主服务器的资源,那么您可以轻松地将其转移。

它还允许您批量处理您尝试执行的任何任务(发送电子邮件/无论如何)。

答案 3 :(得分:4)

我绝对认为在ASP.NET中快速,低优先级异步工作的一般做法是使用.NET线程池,特别是对于高流量场景,因为您希望资源有限。

此外,线程的实现是隐藏的 - 如果您开始生成自己的线程,您还必须正确管理它们。不是说你不能这样做,而是为什么重新发明那个轮子?

如果性能成为问题,并且您可以确定线程池是限制因素(而不是数据库连接,传出网络连接,内存,页面超时等),那么您调整线程池配置以允许更多工作线程,更高的排队请求等。

如果您没有性能问题,那么选择生成新线程以减少与ASP.NET请求队列的争用是经典的过早优化。

理想情况下,您不需要使用单独的线程来执行日志记录操作 - 只需启用原始线程即可尽快完成操作,这是MSMQ和单独的消费者线程/进程进入图片。我同意这更重要,更多的工作要实现,但你真的需要耐用​​性 - 共享的内存队列的波动性将很快受到欢迎。

答案 4 :(得分:2)

您应该使用QueueUserWorkItem,并避免像避免瘟疫一样创建新线程。对于一个可以解释为什么你不会饿死ASP.NET的视觉效果,因为它使用相同的ThreadPool,想象一个非常熟练的变戏法者用两只手来保持六打保龄球,剑或飞行中的任何东西。为了了解为什么创建自己的线程很糟糕,想象一下在西雅图高峰时段发生的事情,当高速公路大量使用入口坡道时,车辆立即进入交通状态而不是使用灯光,并且每隔几秒就限制一次入口数量。 。最后,有关详细说明,请参阅此链接:

http://blogs.msdn.com/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx

谢谢, 托马斯

答案 5 :(得分:1)

那篇文章不正确。 ASP.NET拥有自己的线程池,托管工作线程,用于提供ASP.NET请求。这个池通常是几百个线程,与ThreadPool池分开,后者是处理器的一小部分。

在ASP.NET中使用ThreadPool不会干扰ASP.NET工作线程。使用ThreadPool很好。

设置单个线程也是可以接受的,该线程仅用于记录消息并使用生产者/消费者模式将日志消息传递给该线程。在这种情况下,由于线程是长期存在的,因此您应该创建一个新线程来运行日志记录。

为每条消息使用新线程肯定是矫枉过正。

另一种选择,如果你只是谈论日志记录,就是使用像log4net这样的库。它处理单独线程中的日志记录,并处理该场景中可能出现的所有上下文问题。

答案 6 :(得分:1)

我会说这篇文章错了。如果您正在运行一个大型.NET商店,您可以安全地在多个应用程序和多个网站(使用单独的应用程序池)中使用该池,只需基于ThreadPool文档中的一个语句:

  

每个进程有一个线程池。   线程池的默认大小为   每个可用250个工作线程   处理器,1000 I / O完成   线程。中的线程数   线程池可以通过使用来更改   SetMaxThreads方法。每个线程   使用默认堆栈大小并运行   在默认优先级。

答案 7 :(得分:1)

上周我在工作中被问到一个类似的问题,我会给你相同的答案。为什么每个请求都有多线程Web应用程序? Web服务器是一个非常出色的系统,可以及时提供许多请求(即多线程)。想想当您在网上请求几乎任何页面时会发生什么。

  1. 请求某个页面
  2. Html被送回
  3. Html告诉客户进一步要求(js,css,图像等)。
  4. 提供更多信息
  5. 您举例说明了远程日志记录,但这应该是您的记录器的一个问题。应该有一个异步过程来及时接收消息。 Sam甚至指出你的记录器(log4net)应该已经支持了这个。

    Sam也是正确的,因为使用CLR上的线程池不会导致IIS中的线程池出现问题。但是,这里要关注的是,您不是从进程中生成线程,而是从IIS线程池线程生成新线程。存在差异,区别很重要。

      

    主题与流程

         

    线程和进程都是方法   并行化应用程序。   但是,流程是独立的   包含自己的执行单元   国家信息,使用自己的   地址空间,只与之交互   彼此通过进程间   沟通机制(一般而言   由操作系统管理)。   申请通常是分开的   在设计过程中进入过程   阶段和显式的主进程   它产生时产生子进程   逻辑分离的感觉   重要的应用功能。   换句话说,过程是一个过程   建筑结构。

         

    相比之下,线程是一种编码   不影响的构造   应用程序的体系结构。一个   单个进程可能包含多个   线程;进程中的所有线程   共享相同的状态和相同的内存   空间,并可以与每个人沟通   其他直接,因为他们分享   相同的变量。

    Source

答案 8 :(得分:0)

我不同意引用的文章(C#feeds.com)。创建一个新线程很容易但很危险。在单个内核上运行的最佳活动线程数实际上非常低 - 小于10.如果为次要任务创建线程,则太容易导致机器浪费时间切换线程。线程是REQUIRE管理的资源。 WorkItem抽象是为了解决这个问题。

在减少可用于请求的线程数和创建太多线程以允许任何线程有效处理之间存在折衷。这是一个非常动态的情况,但我认为应该主动管理(在这种情况下由线程池)而不是将其留给处理器以保持领先于线程的创建。

最后,本文对使用ThreadPool的危险做了一些非常全面的陈述,但它确实需要一些具体的东西来支持它们。

答案 9 :(得分:0)

IIS是否使用相同的ThreadPool来处理传入的请求似乎很难获得明确的答案,而且似乎已经更改了版本。因此,不要过度使用ThreadPool线程似乎是一个好主意,因此IIS有很多可用的。另一方面,为每个小任务生成自己的线程似乎是个坏主意。据推测,您在日志记录中有某种锁定,因此一次只能有一个线程进行,其余线程只需轮流进行调度和未调度(更不用说产生新线程的开销)。从本质上讲,您遇到了ThreadPool旨在避免的确切问题。

似乎合理的妥协是你的应用程序分配一个你可以传递消息的日志记录线程。您可能需要小心,尽可能快地发送消息,这样您就不会减慢应用程序的速度。

答案 10 :(得分:0)

您可以使用Parallel.For或Parallel.ForEach并定义要分配的线程限制,以使它们平稳运行并防止池不足。

但是,在后台运行时,您将需要在ASP.Net Web应用程序中使用下面的纯TPL样式。

var ts = new CancellationTokenSource();
CancellationToken ct = ts.Token;

ParallelOptions po = new ParallelOptions();
            po.CancellationToken = ts.Token;
            po.MaxDegreeOfParallelism = 6; //limit here

 Task.Factory.StartNew(()=>
                {                        
                  Parallel.ForEach(collectionList, po, (collectionItem) =>
                  {
                     //Code Here PostLog(logEvent);
                  }
                });