线程池极限性能延迟

4
有关这个问题已经有很多讨论,但他们似乎无法解释我的特定问题。使用ThreadPool而不是Thread类进行线程处理时,我遇到了严重的性能问题。
细节如下:
我建立了一个TCP服务器,当TCP服务器接受新客户端时,它会生成一个新线程来处理该客户端。非常简单,但是当服务器需要处理许多并发客户端时,它处理起来需要太长时间。对于大约35个只发送2048字节缓冲区、接收并关闭的简单客户端,需要30秒才能处理完毕。
经过多次计时,我发现ThreadPool.QueueUserWorkItem最多需要26秒。我使用它来为处理新客户端生成新线程。 将ThreadPool.QueueUserWorkItem替换为new Thread()后,我的性能提高到不到一秒。
我希望能够得到一些解释为什么会出现这种情况。
澄清: 延迟与客户端代码无关,从调用ThreadPool.QueueUserWorkItem到clientMsgHandler.HandleIncomingMsgs启动可以花费20秒。 滞后始于第一个线程,并随着测试的进行略微改善。我更感兴趣的是解释为什么会出现这种情况,而不是解决方案。 客户端确实会阻塞,但只有很短的时间。
服务器代码:
private void AddTcpClientMsgHandler(TcpClient tcpClient)
    {
        //lock so no addition of client and closure can occur concurrently
        Stopwatch watch = new Stopwatch();
        watch.Start();
        Monitor.Enter(this);
        int pWatchIdx =  watchIDX++;
        if (!isOpen)
            throw new ObjectDisposedException(ResourceAlreadyClosed);

        TcpClientMsgHandler clientMsgHandler = CreateClientHandler(tcpClient);                                         
        clientMsgHandlerManager.AddTcpClientMsgHandler(clientMsgHandler);
        //ThreadPool.QueueUserWorkItem(clientMsgHandler.HandleIncomingMsgs); takes 20 seconds to run
        Thread thread = new Thread(clientMsgHandler.HandleIncomingMsgs);
        thread.Start();
        watch.Stop();
        Monitor.Exit(this);
        Console.WriteLine(string.Format("Iteration {0} took {1} Client {2}", pWatchIdx.ToString(),watch.Elapsed, tcpClient.Client.RemoteEndPoint));

    }

尝试使用性能分析器,例如Red Gate的ANTS Performance Profiler - Uwe Keim
你的线程池可能已经用完了线程? - Rudi Visser
2
通用的诊断是你的HandleIncomingMsgs()方法花费了太长时间。顺便提一下,在finally块中不使用Monitor.Exit()是一个严重的错误。 - Hans Passant
1
为什么不使用TPL的Task.Factory.StartNew方法,配合一个具有设置并发级别的自定义TaskScheduler(http://msdn.microsoft.com/en-us/library/dd781658(v=vs.100).aspx)? - Max Yakimets
Rudi,不,我正在使用一台强大的计算机,并且只打开了不到100个线程。 Hans,这并不需要很长时间,实际上只需要不到一毫秒。但是感谢你提醒我关于finally的问题,我会做好准备的。 Max,我会去检查一下的。 - lavuy
4个回答

5

阻塞代码是线程池的敌人。从你发布的示例中,无法确定阻塞发生在哪里,但我建议你回顾一下代码路径,找出代码哪里被阻塞了。在调试器中运行服务器直到开始出现高延迟,然后中断执行并查看VS的线程面板。这将显示线程被阻塞的位置。很可能是因为同步IO引起的。考虑改用异步代码。


你可能是对的。我尝试过这样做,但在线程池中运行时阻塞线程会导致巨大的延迟。你能解释一下吗? - lavuy

1

ThreadPool.QueueUserWorkItem - 将方法排队以执行。当线程池线程可用时,该方法将执行。

  • 线程池等待空闲线程。
  • 当找到空闲线程时,线程池使用它来执行您的方法。

为什么ThreadPool很慢?

  • 除非您在ThreadPool中用完线程,否则以上两个原因并不是真正的原因。
  • 在.NET 3.5中,有2000个工作线程和1000个IO完成端口线程。(在4.0、4.5中更多)。请查看Jon Skeet's的答案(线程池中的活动线程数)。

[例如]

.Net 2.0默认每个可用处理器有25个线程。这意味着如果您排队了30个任务,则最后5个任务必须等待线程从池中变得可用才能执行。

解决方案:

SetMinThreads() 将最小线程数设置为30(对于 .Net 2.0)。 这将提高性能,因为ThreadPool不会立即在需要时创建新线程;它只在某些间隔上这样做。

注意:每个客户端使用一个线程不支持更多的并发。
使用异步套接字 - 这些是非阻塞套接字,除了您不必轮询外,堆栈还会在发生“有趣”的事情时向程序发送特殊的窗口消息。

我的进程在四核CPU上最多有50个线程,这不符合实际。 - lavuy
显然,线程不会短缺 :) 你可以考虑使用哪种I/O策略?是阻塞式还是非阻塞式I/O(你的HandleIncomingMsgs是什么)? - C-va

0
在线程池中使用了前几个线程之后,系统会在启动每个新的线程池线程之前引入延迟(这不影响重新使用的线程)。
您可以通过在启动任何线程之前将 ThreadPool.SetMinThreads 设置为适当的大值来更改线程池线程的初始数量。但是你不应该那样做!(所以你没从我这里听到... ;))
您应该探索一种减少线程数量的方法,而不是那样做。

糟糕,你的服务器刚刚爆炸了。 - spender
关闭。我上次听说是0.5秒的延迟,在服务器应用程序中,您可以稍微调整MinThreads。 - H H
没错。正确的方法是改变实现方式,使其不使用太多线程。那个答案的范围超出了我现在回复的时间! - Matthew Watson

0

我认为这取决于你在clientMsgHandler.HandleIncomingMsgs()方法中正在做什么。

线程池只能用于非常短的处理。

此外,线程池每个可用处理器有默认的25个工作线程,请注意您的线程中的交叉锁定。

>>托管线程池


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接