.NET CLR ThreadPool耗尽 - 实现错误?

时间:2014-04-21 00:22:04

标签: c# .net mono clr async-await

我编写了一个简单的基于异步的负载测试库,它还有一个控制台界面,可以从命令行进行测试。

基本上,它同时运行大量请求,聚合它们,并显示摘要和简单的直方图。没有什么花哨。但是我在本地系统中运行了很多测试,因此我想确保测试工具尽可能地使用尽可能少的资源来获得相对准确的基准测试。因此它使用与开始/结束方法的裸异步来保持最小的开销。

所有完成,完全异步,它可以工作,并且不受影响(大多数情况下)。但是正常会话中的线程数远远超过40个。因此,考虑到本地计算机正在运行正在测试的服务器,对于具有4个硬件线程的计算机来说,这是一个非常简洁的资源浪费。

我已经在AsyncContext中运行该程序,它基本上只是一个简单的排队上下文,将所有内容放在同一个线程上。因此,所有异步回发都在主线程上。完美。

现在,我所要做的就是限制ThreadPool的最大线程,看看它的执行情况。将它限制在实际核心,有4个工作线程和4个IOCP线程。

  

结果吗

     

例外:“ThreadPool中没有足够的可用线程    完成操作。“

嗯,这不是一个新问题,并且遍布互联网。但是不是ThreadPool的全部内容,你可以把事情放到池的队列中,并且只要线程可用就会执行它?

事实上,该方法的名称是“队列”UserWorkItem。文档适当地说,“将一个方法排队执行。该方法在线程池线程可用时执行。”

现在,如果没有足够的可用线程可用,理想情况下,预期的可能是,程序执行速度减慢。 IOCP和异步任务应该排在队列中,但为什么它以这样的方式实现,它会被击倒,而失败呢?当它被称为ThreadPool是一个队列时,增加线程数不是解决方案。

  

修改 - 澄清:

     

我完全了解线程池的概念,以及CLR的原因   旋转更多线程。这应该。我同意,当存在大量IO绑定任务时,这是正确的事情。但关键是,如果你做了事实上的限制   ThreadPool中的线程,它应该为任务排队   每当一个空闲线程可用时执行,而不是抛出异常。   并发性可能会受到影响,甚至可能会减慢并发性   结果,但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文章稍微修改过的代码,在Windows 8.1中的.NET 4.5.1下应该会很快失败。

(随意更改网址和线程限制)。

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();
}

对此行为的任何洞察都可以为此提供推理,我们非常感谢。感谢。

1 个答案:

答案 0 :(得分:8)

在您的示例代码中,不是调用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 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框架的一部分)也使用的共享资源 - 将最大线程数设置为2是Raymond Chen称之为全局解决方案一个本地问题,因此打破了系统其他部分的期望。

如果你想明确控制正在使用的线程,那么你应该创建自己的实现,但是除非你真的知道自己在做什么,否则最好让.Net框架处理线程管理。