问题描述
我编写了一个简单的基于异步的负载测试库,它还有一个可以从命令行进行测试的控制台界面。 基本上,它并发运行大量请求,聚合它们,并显示摘要和简单的直方图。没什么花哨的。但是我在本地系统中运行了很多测试,所以我想确保测试工具尽可能使用最少的资源来实现相对准确的基准测试。因此,它使用带有BEGIN/END方法的纯异步来维护最低开销。
全部完成,完全异步,它可以工作,并且不会妨碍工作(好吧,大部分情况下)。但是,正常会话中的线程数量远远超过40个。因此,考虑到本地计算机也在运行被测试的服务器,对于具有4个硬件线程的计算机来说,这是非常浪费资源的。
我已经在AsyncContext中运行该程序,它基本上只是一个简单的排队上下文,将所有内容放到同一个线程上。所以,所有aync回发都在主线程上。完美。
现在,我要做的就是限制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中的.NET 4.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:UsersJustinSourceReposAzureConsoleApplication1ConsoleApplication1Program.cs:line 39
InnerException:
实际上,如果我们查看HttpWebRequest.BeginGetResponse
method,我们可以看到以下
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 Framework的一部分)也使用的共享资源-将最大线程数设置为2是Raymond Chen所说的本地问题的全局解决方案,因此打破了系统其他部分的预期。
如果您希望显式控制正在使用的线程,则应该创建您自己的实现,但是,除非您确实知道自己在做什么,否则最好让.Net Framework处理线程管理。
这篇关于.NET CLR线程池耗尽-实现错误?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!