.NET CLR线程池耗尽-实现错误
本文关键字:实现 错误 CLR 线程 NET | 更新日期: 2023-09-27 17:59:12
我写了一个简单的基于异步的负载测试库,它还有一个控制台接口,可以从命令行进行测试。
基本上,它同时运行大量请求,聚合它们,并显示摘要和简单的直方图。没什么新奇的。但我在本地系统中运行了很多测试,所以我想确保测试工具能够使用尽可能少的资源来获得相对准确的基准测试。因此,它使用带有Begin/End方法的裸异步来保持最少的开销。
所有这些都完成了,完全异步,它可以工作,而且不会妨碍(好吧,大部分)。但正常会话中的线程数远远超过40个。因此,对于一台有4个硬件线程的机器来说,考虑到本地机器也在运行被测试的服务器,这是一种非常巧妙的资源浪费。
我已经在AsyncContext中运行了该程序,它基本上只是一个简单的排队上下文,将所有内容放到同一个线程上。所以,所有aync post-back都在主线程上。完美的
现在,我所要做的就是限制ThreadPool的最大线程数,看看它的性能如何。将其限制为实际的核心,有4个工作线程和4个IOCP线程。
结果
异常:"线程池中没有足够的可用线程以完成操作。"
嗯,这不是一个新问题,而且在互联网上非常分散。但是ThreadPool的全部意义不就是,你可以把东西放在池的队列上,只要有线程可用,它就会执行吗?
实际上,方法的名称是"Queue"UserWorkItem。文档中适当地写道:"将一个方法排队等待执行。当线程池线程可用时,该方法就会执行。"
现在,如果没有足够的可用线程,理想情况下,预期的可能是程序执行速度减慢。IOCP和异步任务应该只是排队,但为什么它的实现方式会让它崩溃而失败呢?当它被称为线程池(ThreadPool)时,增加线程数量并不是解决方案。
编辑-澄清:
我完全了解线程池的概念,以及为什么CLR旋转更多的线程。它应该。我同意,当有繁重的IO任务时,这实际上是正确的做法。但关键是,如果你确实限制了线程池中的线程,则预期将任务排队等待在空闲线程可用时执行,而不是抛出异常。并发性可能会受到影响,甚至可能会减慢结果,但QueueWorkUserItem旨在排队,而不是仅工作当一个新线程可用或失败时,我推测这是一个实现错误,如标题所述。
更新1:
与Microsoft的支持论坛中记录的问题相同,示例如下:http://support.microsoft.com/default.aspx?scid=kb;EN-US;815637
建议的解决方法显然是增加线程数量,因为它无法排队。
注意:这是在一个非常旧的运行时下进行的,下面给出了在4.5.1运行时上重现相同问题的方法
更新2:
在Mono Runtime上运行相同的代码片段,ThreadPool似乎没有问题。它被排队并执行。此问题仅在Microsoft CLR下发生。
更新3:
在@Noseratio指出无法在.NET 4.5.1下重现相同代码的有效问题后,下面是一段将重现该问题的代码。为了按预期中断排队时工作的代码,真正需要做的就是向排队的委托添加一个真正的异步调用。
例如,只需将下面的行添加到委托的末尾,就会出现异常:
(await WebRequest.Create("http://www.google.com").GetResponseAsync()).Close();
复制代码:
这是一个从MSKB文章中稍微修改过的代码,在Windows8.1中的.NET4.5.1下应该会很快失败。
(请随意更改url和线程限制)
public static void Main()
{
ThreadPool.SetMinThreads(1, 1);
ThreadPool.SetMaxThreads(2, 2);
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Queued {0}", i);
ThreadPool.QueueUserWorkItem(PoolFunc);
}
Console.ReadLine();
}
private static async void PoolFunc(object state)
{
int workerThreads, completionPortThreads;
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
Console.WriteLine(
"Available: WorkerThreads: {0}, CompletionPortThreads: {1}",
workerThreads,
completionPortThreads);
Thread.Sleep(1000);
string url = "http://localhost:8080";
HttpWebRequest myHttpWebRequest;
// Creates an HttpWebRequest for the specified URL.
myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
// Sends the HttpWebRequest, and waits for a response.
Console.WriteLine("Wait for response.");
var myHttpWebResponse = await myHttpWebRequest.GetResponseAsync();
Console.WriteLine("Done.");
myHttpWebResponse.Close();
}
任何对这种行为的深入了解,都会给这件事带来推理,我们将不胜感激。谢谢
在示例代码中,不是对QueueUserWorkItem
的调用引发异常,而是对await myHttpWebRequest.GetResponseAsync()
的调用引发了异常。如果你查看异常细节,你可以确切地看到抛出这个异常的方法
System.InvalidOperationException was unhandled by user code
_HResult=-2146233079
_message=There were not enough free threads in the ThreadPool to complete the operation.
HResult=-2146233079
IsTransient=false
Message=There were not enough free threads in the ThreadPool to complete the operation.
Source=System
StackTrace:
at System.Net.HttpWebRequest.BeginGetResponse(AsyncCallback callback, Object state)
at System.Threading.Tasks.TaskFactory`1.FromAsyncImpl(Func`3 beginMethod, Func`2 endFunction, Action`1 endAction, Object state, TaskCreationOptions creationOptions)
at System.Threading.Tasks.TaskFactory`1.FromAsync(Func`3 beginMethod, Func`2 endMethod, Object state)
at System.Net.WebRequest.<GetResponseAsync>b__8()
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at ConsoleApplication1.Program.<PoolFunc>d__0.MoveNext() in c:'Users'Justin'Source'Repos'Azure'ConsoleApplication1'ConsoleApplication1'Program.cs:line 39
InnerException:
事实上,如果我们观察HttpWebRequest.BeginGetResponse
方法,我们可以看到以下
if (!RequestSubmitted && NclUtilities.IsThreadPoolLow())
{
// prevent new requests when low on resources
Exception exception = new InvalidOperationException(SR.GetString(SR.net_needmorethreads));
Abort(exception, AbortState.Public);
throw exception;
}
这个故事的寓意是,线程池是其他代码(包括.Net框架的部分)也使用的共享资源——将线程的最大数量设置为2是Raymond Chen所说的局部问题的全局解决方案,因此打破了系统其他部分的期望。
如果你想明确控制正在使用的线程,那么你应该创建自己的实现,但是除非你真的知道自己在做什么,否则最好让.Net框架来处理线程管理。