我一直认为将 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 是否意味着您无论如何都会达到资源的限制,在这种情况下,您不应该尝试开始自己的线程吗?
澄清:我只是在小型非关键异步任务(例如,远程日志记录)的范围内询问,而不是需要单独进程的昂贵工作项(在这些情况下,我同意您需要一个更强大的解决方案)。
这里的其他答案似乎忽略了最重要的一点:
除非您尝试并行化 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 完成端口)。
具体来说,您应该使用您正在使用的任何库类支持的异步回调。这些方法总是被非常清楚地标记;它们以 Begin
和 End
开头。如 Stream.BeginRead
、Socket.BeginConnect
、WebRequest.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 中编写线程代码 - 考虑是否可以重写它以使用预先存在的异步方法,如果不能,那么请考虑您是否真的,真的需要代码完全在后台线程中运行。在大多数情况下,您可能会增加复杂性而没有净收益。
根据 Microsoft ASP.NET 团队的 Thomas Marquadt 的说法,使用 ASP.NET 线程池 (QueueUserWorkItem) 是安全的。
Q) 如果我的 ASP.NET 应用程序使用 CLR ThreadPool 线程,我不会饿死 ASP.NET,它也使用 CLR ThreadPool 来执行请求吗? .. A) 总而言之,不要担心 ASP.NET 的线程会饿死,如果您认为这里有问题,请告诉我,我们会处理的。问)我应该创建自己的线程(新线程)吗?这对 ASP.NET 来说不是更好吗,因为它使用 CLR 线程池。 A)请不要。或者换一种说法,不!!!如果你真的很聪明——比我聪明得多——那么你可以创建自己的线程;否则,就别想了。以下是您不应该频繁创建新线程的一些原因: 与 QueueUserWorkItem 相比,它非常昂贵...顺便说一句,如果您可以编写比 CLR 更好的 ThreadPool,我鼓励您申请微软的工作,因为我们肯定在寻找像你这样的人!
网站不应该绕过产生线程。
您通常将此功能移到 Windows 服务中,然后与之通信(我使用 MSMQ 与它们通信)。
- 编辑
我在这里描述了一个实现:Queue-Based Background Processing in ASP.NET MVC Web Application
- 编辑
扩展为什么这比线程更好:
使用 MSMQ,您可以与另一台服务器通信。您可以跨机器写入队列,因此如果您出于某种原因确定您的后台任务过多地占用了主服务器的资源,您可以非常简单地转移它。
它还允许您批处理您尝试执行的任何任务(发送电子邮件/其他)。
我绝对认为,在 ASP.NET 中快速、低优先级异步工作的一般做法是使用 .NET 线程池,特别是对于希望资源受限的高流量场景。
此外,线程的实现是隐藏的——如果你开始产生自己的线程,你也必须正确地管理它们。不是说你做不到,而是为什么要重新发明那个轮子?
如果性能成为问题,并且您可以确定线程池是限制因素(而不是数据库连接、传出网络连接、内存、页面超时等),那么您可以调整线程池配置以允许更多工作线程、更高排队请求, ETC。
如果您没有性能问题,那么选择生成新线程以减少与 ASP.NET 请求队列的争用是典型的过早优化。
理想情况下,您不需要使用单独的线程来执行日志记录操作 - 只需启用原始线程尽快完成操作,这就是 MSMQ 和单独的消费者线程/进程出现的地方。我同意这是更重和更多的工作来实现,但你真的需要这里的持久性 - 共享内存队列的波动性将很快耗尽它的欢迎。
您应该使用 QueueUserWorkItem,并避免像避免瘟疫一样创建新线程。对于解释为什么您不会饿死 ASP.NET 的视觉效果,因为它使用相同的 ThreadPool,想象一个非常熟练的杂耍者用两只手保持半打保龄球瓶、剑或任何东西在飞行中。为了直观地了解为什么创建自己的线程是不好的,想象一下在高峰时间西雅图会发生什么,当高速公路上大量使用的入口坡道允许车辆立即进入交通而不是使用灯并将入口数量限制为每隔几秒一个.最后,详细解释请看这个链接:
谢谢,托马斯
那篇文章不正确。 ASP.NET 有它自己的线程池,托管工作线程,用于服务 ASP.NET 请求。这个池通常有几百个线程,并且与 ThreadPool 池分开,后者是一些较小的处理器倍数。
在 ASP.NET 中使用 ThreadPool 不会干扰 ASP.NET 工作线程。使用 ThreadPool 很好。
设置一个仅用于记录消息并使用生产者/消费者模式将日志消息传递给该线程的线程也是可以接受的。在这种情况下,由于线程是长期存在的,您应该创建一个新线程来运行日志记录。
为每条消息使用一个新线程绝对是矫枉过正。
如果您只讨论日志记录,另一种选择是使用 log4net 之类的库。它在一个单独的线程中处理日志记录,并处理该场景中可能出现的所有上下文问题。
我会说这篇文章是错误的。如果您经营一家大型 .NET 商店,您可以安全地跨多个应用程序和多个网站使用该池(使用单独的应用程序池),只需根据 ThreadPool 文档中的一条语句:
每个进程有一个线程池。线程池的默认大小为每个可用处理器 250 个工作线程和 1000 个 I/O 完成线程。可以使用 SetMaxThreads 方法更改线程池中的线程数。每个线程使用默认堆栈大小并以默认优先级运行。
System.Threading.ThreadPool
公开的线程池相同。
上周我在工作中被问到一个类似的问题,我会给你同样的答案。为什么每个请求都使用多线程 Web 应用程序? Web 服务器是一个非常棒的系统,经过大量优化,可以及时提供许多请求(即多线程)。想想当您请求网络上的几乎任何页面时会发生什么。
对某个页面发出请求 Html 被返回 Html 告诉客户端进行进一步的请求(js、css、图像等)。返回进一步的信息
您给出了远程日志记录的示例,但这应该是您的记录器关注的问题。应该有一个异步过程来及时接收消息。 Sam 甚至指出您的记录器 (log4net) 应该已经支持这一点。
Sam 也是正确的,因为在 CLR 上使用线程池不会导致 IIS 中的线程池出现问题。不过,这里要注意的是,您不是从进程中产生线程,而是从 IIS 线程池线程中产生新线程。有区别,区别很重要。
线程与进程 线程和进程都是并行化应用程序的方法。但是,进程是独立的执行单元,它们包含自己的状态信息,使用自己的地址空间,并且只能通过进程间通信机制(通常由操作系统管理)相互交互。应用程序通常在设计阶段被划分为进程,当逻辑上分离重要的应用程序功能有意义时,主进程会显式生成子进程。换句话说,流程是一种架构构造。相比之下,线程是一种不影响应用程序架构的编码结构。单个进程可能包含多个线程;一个进程中的所有线程共享相同的状态和相同的内存空间,并且可以直接相互通信,因为它们共享相同的变量。
您可以使用 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);
}
});
我不同意引用的文章(C#feeds.com)。创建一个新线程很容易但很危险。在单个内核上运行的最佳活动线程数实际上非常低 - 不到 10。如果线程是为次要任务创建的,那么很容易导致机器浪费时间切换线程。线程是需要管理的资源。 WorkItem 抽象是用来处理这个的。
在减少可用于请求的线程数量和创建太多线程以允许它们中的任何一个有效处理之间存在权衡。这是一个非常动态的情况,但我认为应该积极管理(在这种情况下由线程池)而不是将其留给处理器以保持领先于线程的创建。
最后,这篇文章对使用 ThreadPool 的危险做了一些非常全面的陈述,但它确实需要一些具体的东西来支持它们。
IIS 是否使用相同的 ThreadPool 来处理传入请求似乎很难得到明确的答案,而且似乎也已经改变了版本。因此,不要过度使用 ThreadPool 线程似乎是个好主意,这样 IIS 就有很多可用的线程。另一方面,为每个小任务生成自己的线程似乎是个坏主意。据推测,您的日志记录中有某种锁定,因此一次只能处理一个线程,其余线程将轮流安排和不安排(更不用说产生新线程的开销)。从本质上讲,您遇到了 ThreadPool 旨在避免的确切问题。
似乎一个合理的折衷方案是让您的应用程序分配一个您可以将消息传递到的日志记录线程。您需要注意尽可能快地发送消息,以免降低应用程序的速度。
不定期副业成功案例分享
Action<T>
作为回调的方法来包装它们。如果您的意思是选择使用工作线程还是 I/O 线程发生在最低级别,那是故意的;只有该级别可以决定是否需要 IOCP。ThreadPool
以这种方式限制了您,这可能是因为他们不相信开发人员能做到这一点。非托管 Windows 线程池具有非常相似的 API,但实际上允许您选择线程类型。