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

我一直有这样的印象,即使在 ASP.NET 中,使用 ThreadPool 处理短期的后台任务(比如说非关键的)也被认为是最佳实践,但是后来我遇到了 这篇文章,它似乎给出了相反的建议——理由是你应该离开 ThreadPool 来处理与 ASP.NET 相关的请求。

以下是我到目前为止一直在做的小型异步任务:

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

那篇文章建议明确地创建一个线程,类似于:

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

The first method has the advantage of being managed and bounded, but there's the potential (if the article is correct) that the background tasks are then vying for threads with ASP.NET request-handlers. The second method frees up the ThreadPool, but at the cost of being unbounded and thus potentially using up too many resources.

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

如果你的网站流量太大,你的线程池已经满了,那么是走出带外更好,还是一个完整的线程池意味着你已经达到了资源的极限,在这种情况下,你不应该尝试开始你自己的线程?

Clarification: I'm just asking in the scope of small non-critical asynchronous tasks (eg, remote logging), not expensive work items that would require a separate process (in these cases I agree you'll need a more robust solution).

42956 次浏览

网站不应该到处产生线程。

您通常将此功能转移到 Windows 服务中,然后与之进行通信(我使用 MSMQ 与它们进行通信)。

——编辑

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

——编辑

为了解释为什么这甚至比线程更好:

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

它还允许您批处理任何您试图完成的任务(发送电子邮件或其他任务)。

我确信 ASP.NET 中快速、低优先级异步工作的一般实践是使用。NET 线程池,特别是对于高流量场景,因为您希望您的资源是有界的。

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

如果性能成为一个问题,你可以建立线程池是限制因素(而不是数据库连接、传出网络连接、内存、页面超时等) ,然后你调整线程池配置,允许更多的工作线程,更高的排队请求,等等。

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

理想情况下,您不需要使用单独的线程来执行日志记录操作——只需要让原始线程尽快完成操作,这时 MSMQ 和单独的使用者线程/进程就出现了。我同意这是一项繁重的工作,需要执行更多的工作,但是在这里您确实需要持久性——共享的内存中队列的不稳定性将很快消耗掉它的受欢迎程度。

那篇文章不正确。NET 有自己的线程池,即托管工作线程,用于服务 ASP.NET 请求。这个池通常是几百个线程,与 ThreadPool 池是分开的,ThreadPool 池是一些较小的多个处理器。

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

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

对每条消息使用一个新的线程肯定是过分的。

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

我认为这篇文章是错误的。如果你在经营一家。NET 商店您可以安全地使用池跨多个应用程序和多个网站(使用单独的应用程序池) ,仅仅基于一个声明在 线程池文档:

每个进程有一个线程池。 线程池的默认大小为 每个可用工作线程250个 处理器和1000个 I/O 完成 threads. The number of threads in the 线程池可以通过使用 the SetMaxThreads method. Each thread 使用默认堆栈大小并运行 默认优先级。

上周我在工作中被问到一个类似的问题,我会给你同样的答案。为什么每个请求都要多线程处理 Web 应用程序?Web 服务器是一个非常棒的系统,它进行了大量优化,以及时地提供许多请求(即多线程)。想想当你在网络上请求几乎任何一个页面时会发生什么。

  1. 对某个页面发出请求
  2. HTML 被返回
  3. Html 告诉客户端进一步请求(js、 css、 images 等等)
  4. 进一步的信息将返回

您给出了远程日志记录的示例,但这应该是日志记录器所关心的问题。应该有一个异步进程来及时接收消息。Sam 甚至指出,您的 logger (log4net)应该已经支持这一点。

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

线程与进程

线程和进程都是方法 并行化应用程序。 However, processes are independent 执行单位,其中包含自己的 国家信息,使用自己的 地址空间,并且只与 通过进程间相互作用 沟通机制(一般而言 由操作系统管理)。 应用程序通常是分开的 在设计过程中 phase, and a master process explicitly 产生子进程 逻辑上分离的意义 重要的应用功能。 换句话说,进程是一个 建筑结构。

相比之下,线程是一种编码 构造不影响 应用程序的架构 单个进程可能包含多个 线程; 进程中的所有线程 共享相同的状态和相同的内存 space, and can communicate with each other directly, because they share the 相同的变量。

来源

我不同意引用的文章(C # Feeds.com)。创建新线程很容易,但是很危险。在单个核心上运行的活动线程的最佳数量实际上低得惊人——小于10个。如果为小任务创建线程,那么很容易导致机器浪费时间切换线程。线程是需要管理的资源。WorkItem 抽象就是用来处理这个问题的。

在减少可用于请求的线程数量和创建太多线程以使其中任何线程能够高效处理之间存在一种折衷。这是一个非常动态的情况,但我认为应该主动管理(在这种情况下由线程池管理) ,而不是让处理器在创建线程之前先管理它。

最后,这篇文章对使用 ThreadPool 的危险做了一些非常笼统的陈述,但是它确实需要一些具体的东西来支持它们。

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

似乎一个合理的折衷方案是让你的应用程序分配一个单独的日志线程,你可以将消息传递给它。你要小心,发送消息是尽可能快,这样你就不会慢下来你的应用程序。

您应该使用 QueueUserWorkItem,并避免像避免瘟疫一样创建新线程。为了获得解释为什么你不会饿死 ASP.NET 的视觉效果,因为它使用相同的 ThreadPool,想象一个非常熟练的杂耍演员用两只手保持六个保龄球瓶,剑,或任何在飞行。想知道为什么创建自己的线程是不好的,想象一下在西雅图的高峰时间,当高速公路上使用频繁的入口斜坡允许车辆立即进入交通,而不是使用红绿灯,并限制每几秒钟一个入口的数量时会发生什么。最后,有关详细解释,请参阅以下连结:

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

谢谢, 托马斯

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

Unless you are trying to parallelize a CPU-intensive operation in order to get it done faster on a low-load site, there is no point in using a worker thread at all.

That goes for both free threads, created by new Thread(...), and worker threads in the ThreadPool that respond to QueueUserWorkItem requests.

Yes, it's true, you 可以 starve the ThreadPool in an ASP.NET process by queuing too many work items. It will prevent ASP.NET from processing further requests. The information in the article is accurate in that respect; the same thread pool used for QueueUserWorkItem is also used to serve requests.

But if you are actually queuing enough work items to cause this starvation, then you 应该 be starving the thread pool! If you are running literally hundreds of CPU-intensive operations at the same time, what good would it do to have another worker thread to serve an ASP.NET request, when the machine is already overloaded? If you're running into this situation, you need to redesign completely!

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

具体来说,您应该使用您正在使用的库类所支持的异步回调。这些方法总是有非常清楚的标签; 它们以单词 BeginEnd开始。如 Stream.BeginReadSocket.BeginConnectWebRequest.BeginGetResponse等等。

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

请记住,异步 代表不是这样工作的——它们最终将使用一个工作线程,就像 ThreadPool.QueueUserWorkItem一样。类的内置异步方法。NETFramework 库类,它们能够执行此操作。你可以自己做,但是它很复杂,有点危险,可能超出了这个讨论的范围。

在我看来,这个问题的最佳答案是 不要在 ASP.NET 中使用 ABC0 < strong > 或 背景 Thread实例。这一点都不像在 Windows 窗体应用程序中旋转线程,您这样做是为了保持 UI 的响应性,而不关心它的效率。在 ASP.NET 中,您关心的是 吞吐量,无论您是否使用 ThreadPool,所有这些工作线程上的所有上下文切换都绝对会影响到 杀戮的吞吐量。

如果你发现自己在用 ASP.NET 编写线程代码,请考虑是否可以重写代码以使用已有的异步方法,如果不能,请考虑是否真的需要这些代码在后台线程中运行。在大多数情况下,您可能会增加复杂性而没有任何好处。

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

摘自文章 :

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

A)总而言之,不要担心 让 ASP.NET 的线程处于饥饿状态,如果 you think there’s a problem here let me know and we’ll take care of it.

Q)我应该创建自己的线程吗 这样不是更好吗 因为它使用的是 CLR ThreadPool.

A) Please don’t. Or to put it a 不同的方式,不! ! ! 如果你真的 聪明ー比我聪明得多ー然后是你 can create your own threads; otherwise, don’t even think about it. 下面是一些你应该这样做的理由 不经常创建新线程:

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

您可以使用并行。为或平行。对于每个线程,并定义可能分配的线程的限制,以便顺利运行并防止池饥饿。

但是,在后台运行时,您需要在 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);
}
});