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

Posted

技术标签:

【中文标题】在高流量场景中使用 ASP.NET 中的 ThreadPool.QueueUserWorkItem【英文标题】:Using ThreadPool.QueueUserWorkItem in ASP.NET in a high traffic scenario 【发布时间】:2010-11-22 11:32:50 【问题描述】:

我一直认为,即使在 ASP.NET 中,将 ThreadPool 用于(假设是非关键的)短期后台任务也被认为是最佳实践,但后来我遇到了 this article 这似乎否则建议 - 论点是您应该离开 ThreadPool 来处理与 ASP.NET 相关的请求。

所以到目前为止,我一直在执行小型异步任务:

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

而the article 建议改为显式创建一个线程,类似于:

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+ 安装(我现在没有)线程,然后在排队的工作项中做同样的事情。 【参考方案1】:

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

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

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

是的,这是真的,您可以通过排队太多工作项来使 ASP.NET 进程中的ThreadPool 饿死。它将阻止 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 .

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

【讨论】:

感谢您的详细回复,您说得对,我会尽可能尝试使用异步方法(与 ASP.NET MVC 中的异步控制器结合使用)。在我的示例中,使用远程记录器,这正是我可以做的。这是一个有趣的设计问题,因为它将异步处理一直推到代码的最低级别(即记录器实现),而不是能够从控制器级别(在后一种情况下)决定它,例如,您需要两个记录器实现才能从中进行选择)。 @Michael:如果你想将异步回调提升到更多级别,它通常很容易封装;例如,您可以围绕异步方法创建一个外观,并使用一个使用Action<T> 作为回调的方法来包装它们。如果您的意思是选择使用工作线程还是 I/O 线程发生在最低级别,那是故意的;只有该级别可以决定是否需要 IOCP。 虽然,作为一个兴趣点,只有 .NET ThreadPool 以这种方式限制了您,可能是因为他们不相信开发人员能做到这一点。非托管 Windows 线程池具有非常相似的 API,但实际上允许您选择线程类型。 I/O 完成端口 (IOCP)。 IOCP 的描述不太正确。在 IOCP 中,您有一个静态数量的工作线程,它们轮流处理所有待处理的任务。不要与可以固定或动态大小但每个任务有一个线程的线程池混淆 - 扩展得非常可怕。与 ASYNC 不同,每个任务没有一个线程。 IOCP 线程可能会在任务 1 上工作一点,然后切换到任务 3、任务 2,然后再次返回任务 1。任务会话状态被保存并在线程之间传递。 数据库插入怎么样?是否有 ASYNC SQL 命令(如执行)?数据库插入大约是最慢的 I/O 操作(因为锁定),让主线程等待插入行只是浪费 CPU 周期。【参考方案2】:

Microsoft ASP.NET 团队的 Thomas Marquadt 认为,使用 ASP.NET 线程池 (QueueUserWorkItem) 是安全的。

From the article:

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

A) 总而言之,不用担心 饿死 ASP.NET 的线程,如果 你认为这里有问题 我知道,我们会处理的。

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

A) 请不要。或者说 不同的方式,不!如果你真的 聪明——比我聪明得多——那么你 可以创建自己的线程; 否则,就别想了。 以下是您应该这样做的一些原因 不经常创建新线程:

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

【讨论】:

【参考方案3】:

网站不应围绕产生线程。

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

-- 编辑

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

-- 编辑

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

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

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

【讨论】:

我不同意这种笼统的说法总是正确的——尤其是对于非关键任务。仅出于异步日志记录的目的而创建一个 Windows 服务肯定显得过分。此外,该选项并不总是可用(能够部署 MSMQ 和/或 Windows 服务)。 当然,但这是从网站实现异步任务的“标准”方式(针对其他进程排队主题)。 并非所有异步任务都是平等的,这就是为什么 ASP.NET 中存在异步页面的原因。如果我想从远程 Web 服务中获取结果来显示,我不会通过 MSMQ 来实现。在这种情况下,我正在使用远程帖子写入日志。编写 Windows 服务不适合这个问题,也不能为此连接 MSMQ(我也不能,因为这个特定的应用程序在 Azure 上)。 考虑一下:您正在写入远程主机?如果该主机已关闭或无法访问怎么办?你想重新尝试你的写作吗?也许你会,也许你不会。通过您的实施,很难重试。有了这项服务,它变得非常简单。我很感激您可能无法做到这一点,我会让其他人回答从网站创建线程的具体问题 [即如果您的线程不是背景等],但我正在概述“正确”的方法。我不熟悉 azure,虽然我用过 ec2(你可以在上面安装一个操作系统,所以一切都很好)。 @silky,感谢 cmets。我曾说过“非关键”以避免这种更重量级(但持久)的解决方案。我已经澄清了这个问题,所以很明显我不是在要求围绕排队工作项目的最佳实践。 Azure 确实支持这种类型的场景(它有自己的队列存储)——但排队操作对于同步日志记录来说太昂贵了,所以无论如何我都需要一个异步解决方案。就我而言,我知道失败的陷阱,但我不会添加更多基础设施以防万一这个特定的日志记录提供程序失败 - 我也有其他日志记录提供程序。【参考方案4】:

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

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

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

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

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

【讨论】:

【参考方案5】:

您应该使用 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 的回应有何看法。 我同意 Aaronaught 的观点,并且在我的博文中也这么说。我是这样说的,“为了简化这个决定,你应该只切换[到另一个线程],否则你会在你什么都不做的情况下阻塞 ASP.NET 请求线程。这是一个过度简化,但我正在尝试让决定变得简单。”换句话说,不要为非阻塞计算工作执行此操作,但如果您正在向远程服务器发出异步 Web 服务请求,请执行此操作。听听阿罗诺特! :)【参考方案6】:

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

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

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

为每条消息使用一个新线程绝对是矫枉过正。

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

【讨论】:

@Sam,我实际上正在使用 log4net,但没有看到日志被写入单独的线程 - 是否需要启用某种选项?【参考方案7】:

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

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

【讨论】:

一个在单个进程中运行的应用程序完全有能力让自己崩溃! (或者至少降低其自身的性能足以使线程池成为一个失败的提议。) 所以我猜测 ASP.NET 请求使用 I/O 完成线程(而不是工作线程)——对吗? 来自我在回答中链接的 Fritz Onion 的文章:“这个范例改变了 [从 IIS 5.0 到 IIS 6.0] 在 ASP.NET 中处理请求的方式。而不是将请求从 inetinfo.exe 分派到“ (我的重点) 嗯,我仍然不完全确定...那篇文章是 2003 年 6 月的。如果您从 2004 年 5 月开始阅读这篇文章(诚然仍然很老),它会说“Sleep.aspx 测试page 可用于保持 ASP.NET I/O 线程忙碌”,其中 Sleep.aspx 只会导致当前正在执行的线程休眠:msdn.microsoft.com/en-us/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 Hotfix Rollup Package (之后“所有请求现在都在工作线程上运行”)。更重要的是,该示例非常清楚地表明 ASP.NET 线程池与 System.Threading.ThreadPool 公开的线程池相同。【参考方案8】:

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

    对某个页面发出请求 返回 html Html 告诉客户端进一步请求(js、css、图像等) 提供更多信息

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

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

线程与进程

线程和进程都是方法 并行化应用程序。 但是,进程是独立的 包含自己的执行单元 状态信息,使用自己的 地址空间,并且只与 通过进程间相互 通信机制(通常 由操作系统管理)。 应用程序通常分为 在设计过程中进入流程 阶段,以及明确的主进程 生成子进程时 逻辑上分离的意义 重要的应用程序功能。 换句话说,进程是一个 建筑结构。

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

Source

【讨论】:

@Ty,感谢您的输入,但我很清楚网络服务器的工作原理,它与问题并不真正相关 - 再次,正如我在问题中所说,我不是作为一个架构问题寻求指导。我要求提供具体的技术信息。至于“记录器的关注点”应该已经有一个异步过程 - 你认为异步过程应该如何由记录器实现编写?【参考方案9】:

您可以使用 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);
                  
                );

【讨论】:

【参考方案10】:

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

在减少可用于请求的线程数和创建太多线程以允许它们中的任何一个高效处理之间需要权衡取舍。这是一个非常动态的情况,但我认为应该积极管理(在这种情况下由线程池)而不是将其留给处理器以保持领先于线程的创建。

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

【讨论】:

【参考方案11】:

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

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

【讨论】:

以上是关于在高流量场景中使用 ASP.NET 中的 ThreadPool.QueueUserWorkItem的主要内容,如果未能解决你的问题,请参考以下文章

巨人大哥谈缓存在高并发场景下使用法则

使用 css 在鼠标悬停时更改 asp.net 按钮的颜色

Fiddler 没有从 ASP.NET 网站嗅探 SOAP 流量

如何通过缓存提高Web 场景中的ASP.NET App性能

调试 ASP.NET 应用程序时如何在 Fiddler 中显示本地主机流量?

在高流量环境中使用 kafka producer 时的连接管理