【问题标题】:.NET CLR ThreadPool Exhaustion - Implementation bug?.NET CLR 线程池耗尽 - 实现错误?
【发布时间】:2015-03-02 16:30:13
【问题描述】:

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

基本上,它同时运行大量请求,聚合它们,并显示摘要和简单的直方图。没有什么花哨。但是我在本地系统中运行了很多测试,所以我想确保测试工具能够使用尽可能少的资源来获得相对准确的基准测试。因此,它使用带有 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 文章略微修改的代码,在 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();
}

非常感谢任何对此行为的洞察,这可能会带来推理。谢谢。

【问题讨论】:

  • 所有这些线程是否同时处于活动/忙碌状态?休眠线程对性能影响不大...
  • 您在 ThreadPool 上执行了大量的 I/O 绑定操作,并询问为什么它一直试图启动线程? ... ThreadPool 的全部意义在于它是为您管理的。如果您正在寻找对所有内容的如此细粒度的控制……请自己编写。
  • 恐怕以上两个cmets都与此无关。我完全理解线程池的概念,以及 CLR 启动更多线程的原因。但关键是,如果你确实限制了线程,ThreadPool,只要有空闲线程可用,它就会将任务排队等待执行,而不是抛出异常。并发性可能会受到影响,甚至可能会减慢结果,但 QueueWorkUserItem 旨在队列,仅在新线程可用或失败时才起作用。
  • 你有没有检查过为什么没有空闲线程(仅供未来的读者:debug->windows->threads)?也许您真正的异步代码会休眠/等待并吃掉线程? (用一些随机代码阻塞所有 4 个线程并不难......)
  • 是的。那是我的第一个直觉。我确保没有线程最终直接休眠或阻塞。事实上,它必须等待的任何地方,包括用于限制它异步等待的并发的信号量条件。

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


【解决方案1】:

在您的示例代码中,不是对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 框架处理线程管理。

【讨论】:

  • 我不敢相信我错过了追踪。感谢您指出。但我想知道,ThreadPool 低计数检查背后的原因是什么。如果不是为了那个简单的检查,一切都应该按预期工作。除了立即触发请求(理想情况下应该是单独的显式方法或标志,如果是的话)之外,我想不出一个好的理由。
  • @Justin,你打败了我,删除了我的答案 :) 仅供参考,你不必反编译:referencesource.microsoft.com/#System/net/System/Net/…
  • @Noseratio 感谢您的链接 - 它甚至附带解释检查的评论!
  • @Noseratio - 是的。谢谢 - 对你自己和贾斯汀。 :) .. 禁用检查的某种标志将是一个更有用的实现,而不是仅在线程可用性时强制执行请求。
  • @pvl,我建议您尽可能使用新的便携式HttpClient,忘记WebRequest(除非您需要file://ftp:// 协议支持)。
猜你喜欢
  • 2015-02-19
  • 1970-01-01
  • 2016-12-26
  • 2013-06-09
  • 1970-01-01
  • 2015-01-18
  • 2017-07-10
  • 2011-11-28
  • 1970-01-01
相关资源
最近更新 更多