ChatGPT解决这个技术问题 Extra ChatGPT

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

我一直认为将 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 是否意味着您无论如何都会达到资源的限制,在这种情况下,您不应该尝试开始自己的线程吗?

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

情节变厚了 - 我找到了这篇文章 (blogs.msdn.com/nicd/archive/2007/04/16/…),但我无法完全解码。一方面,它似乎是说 IIS 6.0+ 总是处理线程池工作线程上的请求(早期版本可能这样做),但是有这样的:“但是,如果你使用新的 .NET 2.0 异步页面 (Async="true") 或 ThreadPool.QueueUserWorkItem(),则处理的异步部分将在 [完成端口线程] 内完成。” 处理的异步部分
另一件事 - 这应该很容易在 IIS 6.0+ 安装(我现在没有)上进行测试,方法是检查线程池的可用工作线程是否低于其最大工作线程,然后在队列中执行相同操作工作项目。

A
Aaronaught

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

除非您尝试并行化 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 周期。
j
jessehouwing

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

From the article

Q) 如果我的 ASP.NET 应用程序使用 CLR ThreadPool 线程,我不会饿死 ASP.NET,它也使用 CLR ThreadPool 来执行请求吗? .. A) 总而言之,不要担心 ASP.NET 的线程会饿死,如果您认为这里有问题,请告诉我,我们会处理的。问)我应该创建自己的线程(新线程)吗?这对 ASP.NET 来说不是更好吗,因为它使用 CLR 线程池。 A)请不要。或者换一种说法,不!!!如果你真的很聪明——比我聪明得多——那么你可以创建自己的线程;否则,就别想了。以下是您不应该频繁创建新线程的一些原因: 与 QueueUserWorkItem 相比,它非常昂贵...顺便说一句,如果您可以编写比 CLR 更好的 ThreadPool,我鼓励您申请微软的工作,因为我们肯定在寻找像你这样的人!


C
Community

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

您通常将此功能移到 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,感谢您的评论。我曾说过“非关键”以避免这种更重量级(但持久)的解决方案。我已经澄清了这个问题,所以很明显我不是在要求围绕排队工作项目的最佳实践。 Azure 确实支持这种类型的场景(它有自己的队列存储)——但排队操作对于同步日志记录来说太昂贵了,所以无论如何我都需要一个异步解决方案。就我而言,我知道失败的陷阱,但我不会添加更多的基础设施,以防万一这个特定的日志记录提供程序失败 - 我也有其他日志记录提供程序。
S
Sam

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

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

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

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

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


T
Thomas

您应该使用 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 服务请求,请执行此操作。听听阿罗诺特! :)
S
Samuel Neff

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

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

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

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

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


@Sam,我实际上正在使用 log4net 并且没有看到日志被写入单独的线程中 - 我需要启用某种选项吗?
C
Christopher Nobles

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

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


在单个进程中运行的一个应用程序完全有能力让自己崩溃! (或者至少降低其自身的性能足以使线程池成为一个失败的提议。)
所以我猜测 ASP.NET 请求使用 I/O 完成线程(而不是工作线程)——对吗?
我在回答中链接了 Fritz Onion 的文章:“这个范例改变了 [从 IIS 5.0 到 IIS 6.0] 在 ASP.NET 中处理请求的方式。而不是将请求从 inetinfo.exe 分派到 ASP.NET 工作进程,http。 sys 直接在适当的进程中对每个请求进行排队。因此,现在所有请求都由从 CLR 线程池中提取的工作线程提供服务,而不是在 I/O 线程上。 (我的重点)
嗯,我仍然不完全确定...那篇文章是 2003 年 6 月的。如果您阅读 2004 年 5 月的这篇文章(诚然仍然很老),它说“Sleep.aspx 测试页可用于保持 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 公开的线程池相同。
T
Ty.

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

对某个页面发出请求 Html 被返回 Html 告诉客户端进行进一步的请求(js、css、图像等)。返回进一步的信息

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

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

线程与进程 线程和进程都是并行化应用程序的方法。但是,进程是独立的执行单元,它们包含自己的状态信息,使用自己的地址空间,并且只能通过进程间通信机制(通常由操作系统管理)相互交互。应用程序通常在设计阶段被划分为进程,当逻辑上分离重要的应用程序功能有意义时,主进程会显式生成子进程。换句话说,流程是一种架构构造。相比之下,线程是一种不影响应用程序架构的编码结构。单个进程可能包含多个线程;一个进程中的所有线程共享相同的状态和相同的内存空间,并且可以直接相互通信,因为它们共享相同的变量。

Source


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

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

T
Timbo

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

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

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


b
bmm6o

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

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


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅