在高流量情况下在ASP.NET中使用ThreadPool.QueueUserWorkItem


111

我一直给人的印象是,即使在ASP.NET中,也将ThreadPool用于(例如,非关键的)短期后台任务被认为是最佳实践,但随后我发现这篇文章似乎暗示了其他方面-争论是您应该离开ThreadPool来处理与ASP.NET相关的请求。

因此,到目前为止,这是我一直在执行小型异步任务的方式:

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

文章的建议,而不是明确地创建一个线程,类似于:

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

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

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

如果您的网站流量太多,以至于ThreadPool变满了,那么最好带外使用,或者完整的ThreadPool暗示您无论如何都会达到资源的极限,在这种情况下,不应该尝试启动自己的线程?

澄清:我只是在询问小型非关键性异步任务(例如,远程日志记录),而不是需要单独流程的昂贵工作项(在这种情况下,我同意您需要一个更强大的解决方案)。


剧情变厚了-我发现了这篇文章(blogs.msdn.com/nicd/archive/2007/04/16/…),我无法完全解码。一方面,似乎是在说IIS 6.0+总是在线程池工作线程上处理请求(并且早期版本可能会处理),但是这是这样的:“但是,如果您使用的是新的.NET 2.0异步页面( Async =“ true”)或ThreadPool.QueueUserWorkItem(),则处理的异步部分将在[完成端口线程]中完成。” 处理的异步部分
杰夫·斯特恩

另一件事-通过检查线程池的可用工作线程是否低于其最大工作线程,然后在排队的队列中进行相同的操作,这应该很容易在IIS 6.0+安装上进行测试(我现在还没有)。工作项目。
杰夫·斯坦恩

Answers:


104

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

除非您试图并行化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完成端口)。

具体来说,您应该使用所使用的任何库类支持的异步回调。这些方法总是非常清楚地标记;他们以Begin和开头End。如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 Forms应用程序中扩展线程那样,您可以在其中执行此操作以保持UI响应能力,而不在乎它的效率。在ASP.NET中,您关心的是吞吐量,无论使用与否,所有这些工作线程上的所有上下文切换都绝对会杀死吞吐量ThreadPool

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


感谢您的详细答复,您是对的,我会尝试并尽可能使用异步方法(与ASP.NET MVC中的异步控制器结合使用)。就我的示例而言,使用远程记录器,这正是我所能做的。这是一个有趣的设计问题,因为它会将异步处理一直推到代码的最低级别(即记录器实现),而不是能够从控制器级别来决定(在后一种情况下) ,例如,您需要两个记录器实现才能选择)。
Michael Hart 2010年

@Michael:如果您想将异步回叫推到更高的级别,通常很容易包装它;例如,您可以围绕异步方法创建一个外观,并使用单个方法将其包装Action<T>为回调。如果您是选择使用工作线程还是I / O线程是最低级别的选择,那是有意的;只有该级别才能决定是否需要IOCP。
Aaronaught

尽管,作为一个兴趣点,只有.NET ThreadPool这样限制您,这可能是因为他们不信任开发人员来正确实现它。非托管Windows线程池具有非常相似的API,但实际上允许您选择线程类型。
Aaronaught

2
I / O完成端口(IOCP)。IOCP的描述不太正确。在IOCP中,您有一个固定数量的工作线程,该线程轮流处理所有挂起的任务。不要与线程池混淆,线程池的大小可以是固定的,也可以是动态的,但每个任务只有一个线程-可扩展。与ASYNC不同,您每个任务没有一个线程。IOCP线程可能会对任务1起作用,然后切换到任务3,任务2,然后再次回到任务1。任务会话状态被保存并在线程之间传递。
MickyD 2012年

1
那数据库插入呢?是否有ASYNC SQL命令(如Execute)?数据库插入大约是最慢的I / O操作(由于锁定),并且让主线程等待要插入的行只是浪费CPU周期。
伊恩·汤普森

45

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

从文章

问:如果我的ASP.NET应用程序使用CLR ThreadPool线程,我是否会饿死同时使用CLR ThreadPool执行请求的ASP.NET? ..

答:总而言之,不必担心ASP.NET线程会饿死,如果您认为这里有问题,请告诉我,我们会解决。

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

A)请不要。或换一种说法,不!!!!如果您真的很聪明(比我聪明得多),那么您可以创建自己的线程。否则,甚至不要考虑它。以下是一些您不应该经常创建新线程的原因:

  1. 与QueueUserWorkItem相比,这是非常昂贵的。。。顺便说一句,如果您可以编写比CLR更好的ThreadPool,我建议您申请Microsoft的工作,因为我们肯定在寻找喜欢您的人!

4

网站不应绕过产生线程。

通常,您可以将此功能移到与之通信的Windows服务中(我使用MSMQ与他们交谈)。

-编辑

我在这里描述了一个实现:ASP.NET MVC Web应用程序中基于队列的后台处理

-编辑

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

使用MSMQ,您可以与另一台服务器通信。您可以在计算机之间写入队列,因此,如果由于某种原因确定后台任务占用了主服务器的资源过多,则可以轻松地转移它。

它还允许您批处理您要执行的任何任务(发送电子邮件/执行任何操作)。


4
我不同意这个笼统的说法总是正确的-尤其是对于非关键任务。创建Windows服务(仅用于异步日志记录)绝对显得过分。此外,该选项并非始终可用(能够部署MSMQ和/或Windows服务)。
迈克尔·哈特

可以,但这是从网站实施异步任务的“标准”方式(针对其他过程的排队主题)。
中午,丝绸

2
并非所有异步任务都是平等创建的,因此,例如,ASP.NET中存在异步页面。如果我想从远程Web服务中获取结果以显示,则不会通过MSMQ进行操作。在这种情况下,我正在使用远程帖子写日志。编写Windows Service或为此连接MSMQ都不适合该问题(并且由于该特定应用程序位于Azure上,所以我也不能这样做)。
迈克尔·哈特

1
考虑:您正在写入远程主机吗?如果该主机已关闭或无法访问怎么办?您想重试写入吗?也许您会,也许您不会。通过您的实施,很难重试。有了这项服务,它变得微不足道了。非常感谢您可能无法执行此操作,我将让其他人回答有关从网站创建线程的具体问题(即,如果您的线程不是背景等),但是我概述了“正确”的方法方法。尽管我使用过ec2(我可以在上面安装OS,所以一切都很好),但我对azure并不熟悉。
中午,丝绸

@silky,感谢您的评论。我说过“非关键”是为了避免这种重量级(至今仍很耐用)的解决方案。我已经澄清了这个问题,因此很明显,我并不是在要求排队的工作项具有最佳实践。Azure确实支持这种情况(它有自己的队列存储)-但是排队操作对于同步日志记录来说太昂贵了,因此无论如何我都需要一个异步解决方案。就我而言,我知道失败的隐患,但是我不会添加更多的基础架构,以防万一该特定的日志记录提供程序发生故障-我也有其他日志记录提供程序。
迈克尔·哈特

4

我绝对认为,在ASP.NET中进行快速,低优先级异步工作的一般做法是使用.NET线程池,尤其是对于希望限制资源的高流量方案。

而且,线程的实现是隐藏的-如果您开始生成自己的线程,则也必须正确管理它们。不是说你做不到,而是为什么要重新发明轮子呢?

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

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

理想情况下,尽管您不需要使用单独的线程来执行日志记录操作-只需启用原始线程即可尽快完成操作,这就是MSMQ和单独的使用者线程/进程出现的地方。我同意这是一项繁重的工作,但您确实需要持久性-共享内存队列的易变性会很快耗尽其欢迎。


2

您应该使用QueueUserWorkItem,并避免创建新线程,就像避免瘟疫一样。对于解释为什么您不会饿死ASP.NET(因为它使用相同的ThreadPool)的视觉效果,请想象一个非常熟练的魔术师,用两只手保持六打保龄球,剑或飞行中的任何东西。为了了解为什么创建自己的线程不好的原因,可以想象一下在高峰时段西雅图发生的情况,这是因为繁忙的高速公路入口坡道使车辆可以立即进入交通,而不是使用照明灯,并且将入口数量限制为每隔几秒钟。最后,有关详细说明,请参见以下链接:

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

谢谢托马斯


该链接非常有用,感谢Thomas。我也很想听听您对@Aaronaught的回应的看法。
Michael Hart 2010年

我同意Aaronaught的观点,并在我的博客文章中说了同样的话。我这样说:“为了简化此决定,如果您什么都不做会阻塞ASP.NET请求线程,则只应切换[到另一个线程]。这过于简单化了,但我正在尝试使决定变得简单。” 换句话说,不要在非阻塞计算工作中执行此操作,但是如果要向远程服务器发出异步Web服务请求,请执行此操作。听亚伦诺特!:)
Thomas

1

那篇文章不正确。ASP.NET拥有自己的线程池(托管工作线程),用于服务ASP.NET请求。该池通常有几百个线程,并且与ThreadPool池分开,后者是处理器的较小倍数。

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

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

对每个消息使用新线程绝对是过大的。

如果您仅谈论日志记录,则另一种选择是使用类似log4net的库。它在单独的线程中处理日志,并处理该场景中可能出现的所有上下文问题。


1
@Sam,我实际上正在使用log4net,但没有看到日志在单独的线程中写入-是否需要启用某些选项?
Michael Hart

1

我会说这篇文章是错误的。如果您经营的是大型.NET商店,则可以仅基于ThreadPool文档中的一条语句就可以安全地在多个应用程序和多个网站上使用该池(使用单独的应用程序池):

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


在单个进程中运行的一个应用程序完全有能力崩溃!(或者至少降低了自己的性能,足以使线程池成为失败的命题。)
杰夫·斯坦恩

所以我猜ASP.NET请求使用I / O完成线程(而不是工作线程)-是正确的吗?
Michael Hart

从弗里茨·奥尼翁(Fritz Onion)的文章中,我链接了我的答案:“此范例将[从IIS 5.0更改为IIS 6.0]在ASP.NET中处理请求的方式。HTTP而不是将inetinfo.exe的请求分发到ASP.NET辅助进程。 sys直接在适当的过程中将每个请求排队。因此,所有请求现在都由从CLR线程池中提取的工作线程服务,而永远不在I / O线程上。” (我的重点)
Jeff Sternal

嗯,我还是不太确定。。。那是2003年6月的文章。如果您是从2004年5月开始读这篇文章的(肯定还很老),它说:“ Sleep.aspx测试页可以用来保存ASP。 .NET I / O线程繁忙”,其中Sleep.aspx只会使当前正在执行的线程进入睡眠状态:msdn.microsoft.com/zh-cn/library/ms979194.aspx-如果有机会,我会看到如果我可以编写该示例并在IIS 7和.NET 3.5上进行测试
迈克尔·哈特

是的,该段文字令人困惑。在该部分的更远处,它链接到一个支持主题(support.microsoft.com/default.aspx?scid=kb;EN-US;816829),该主题澄清了一些事情:在I / O完成线程上运行请求是.NET Framework 1.0 2003年6月ASP.NET 1.1修补程序汇总程序包(此后“所有请求现在都在工作线程上运行”)中已解决的问题。更重要的是,该示例清楚地表明ASP.NET线程池与公开的线程池相同System.Threading.ThreadPool
杰夫·斯坦恩

1

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

  1. 请求某个页面
  2. HTML送回
  3. Html告诉客户端进行进一步重播(js,css,图像等)。
  4. 返回更多信息

您提供了远程日志记录的示例,但这是您的日志记录器所关心的。应该有一个异步过程来及时接收消息。Sam甚至指出您的记录器(log4net)应该已经支持此功能。

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

线程与进程

线程和进程都是并行化应用程序的方法。但是,进程是独立的执行单元,它们包​​含自己的状态信息,使用自己的地址空间,并且仅通过进程间通信机制(通常由操作系统管理)相互交互。在设计阶段,通常将应用程序划分为多个流程,并且在逻辑上将重要的应用程序功能分开时,主流程会显式生成子流程。换句话说,过程是体系结构。

相比之下,线程是一种编码结构,不会影响应用程序的体系结构。一个进程可能包含多个线程。进程中的所有线程共享相同的状态和相同的内存空间,并且可以直接相互通信,因为它们共享相同的变量。

资源


3
@Ty,感谢您的输入,但是我很清楚Web服务器的工作方式,它与问题并没有真正的关系-就像我在问题中所说的那样,我并不是在寻求有关架构的指导问题。我需要特定的技术信息。至于应该已经有一个异步过程的“记录器问题”,您如何看待异步过程应该由记录器实现来编写?
迈克尔·哈特

0

我不同意所引用的文章(C#feeds.com)。创建新线程很容易,但是很危险。实际上,在单个内核上运行的活动线程的最佳数量实际上令人惊讶地低-少于10。如果为次要任务创建线程,那么很容易导致机器浪费时间切换线程。线程是需要管理的资源。WorkItem抽象在那里处理。

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

最后,本文对使用ThreadPool的危险作了一些详尽的陈述,但确实需要一些具体的东西来加以支持。


0

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

似乎合理的折衷办法是让您的应用分配一个可以将消息传递到的日志线程。您需要注意,发送消息的速度应尽可能快,以免降低应用程序的速度。


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