线程中的无限循环导致 CPU 使用率增加到100%

7
我正在实现一个基于ASP.NET Web应用程序的网络聊天平台,并使用类似长轮询的技术。我的意思是,我保留客户端的每个Web请求一段特定的时间(超时)或直到有新消息到达,然后将响应发送给客户端。
我将连接的客户端保存在内存中(字典对象),每当向客户端发送新消息时,我将此消息写入接收方客户端的消息数组中。客户端需要发送请求以获取自己的消息,我将此请求保存在内存中的数组中。
我使用异步HTTP处理程序来监听客户端请求,将Web请求保存在内存中的数组中。我使用线程从内存中不断检查新消息(在为每个客户端创建的字典中)。
我不使用.NET线程池线程来检查新消息或超时的Web请求。我像这样创建线程:
System.Threading.Thread t = new Thread(new ThreadStart(QueueCometWaitRequest_WaitCallback));
t.IsBackground = false;
t.Start();

在每个线程的QueueCometWaitRequest_WaitCallback方法中,我都会进入一个无限循环的while循环:
while (true)
{
...
Thread.Sleep(100);
}

在这个方法中,我会检查每个Web请求的超时或新消息,这些请求也被保存在内存中的数组中。
一切都很顺利,直到我注意到CPU使用率在一段时间后达到了100%(即在第一个连接的客户端之后的几分钟)。 在第一个请求开始时,一切似乎都很正常,我的意思是在向客户端返回响应时,CPU使用率不会超过10%。但随着时间的推移,即使有两个客户端,CPU使用率也会增加到100%。似乎只有在为客户端请求编写响应时,CPU使用率才会达到100%。如果没有客户端剩余,则一切都会恢复正常(CPU使用率约为0%),直到客户端发出新的Web请求。
我不太了解线程,但我对我创建的并且无限工作的新线程持怀疑态度。就好像操作系统会在它们一直运行时为它们提供更多的CPU使用率和资源,而Thread.Sleep(100)却不起作用。
以下是QueueCometWaitRequest_WaitCallback()方法:
void QueueCometWaitRequest_WaitCallback()
{
   while (true)
   {
      if (processRequest.Length == 0)
      {
          Thread.Sleep(100);
      }
      else
      {
          for (int i = 0; i < processRequest.Length; i++)
          {
               Thread.Sleep(100);

               // below I am checking for new message or request time out 
               .................
               .................

               // If new message or time out I write to response
          }
      }    
   }
}

我希望能够解释这种情况,并且也愿意接受任何建议(比如用不同的方式来实现)。

如果你能帮助我解决这个问题,我将非常感激,谢谢。


1
所以基本上你需要每隔N毫秒检查新消息吗?这就是全部还是还需要异步完成其他操作?如果是的话,只需使用异步的System.Threading.Timer/System.Timers.Timer,它会每隔N毫秒触发一次。 - sll
这大概是你想要实现的吗?while (true){ Thread.Sleep(100); foreach(var req in processRequest){performProcessRequest(req);} processRequest.Remove(r=>r.RequestCompletedOrTimedOut);} - Tetsujin no Oni
@sll:我需要尽快向客户返回响应(新消息)<br/>由于这是一个聊天应用程序,检查周期不应太长,如果我使用计时器,我认为它需要小于1秒? - Mehmet
@Tetsujin no Oni:您说得对,即使没有新消息,我也需要设置超时。 - Mehmet
2个回答

9

作为一般最佳实践的评论,而非直接回答 - 在消息接收器线程中写入Thread.Sleep(100)是不可取的。更好的方法是使用之前提到的Thread.Join或ManualResetEvent等待句柄。例如,您可以编写以下代码:

private ManualResetEvent waitHandle;
private object syncRoot = new object();
private bool isRunning = false;

void CreateThread()
{
    this.waitHandle = new ManualResetEvent(false);

    isRunning = true; // Set to false to kill the thread
    System.Threading.Thread t = new Thread(new ThreadStart(QueueCometWaitRequest_WaitCallback));         
    t.IsBackground = false; 
    t.Start();
}

void PushData()
{
    // On incoming data, push data into the processRequest queue and set the waithandle
    lock(syncRoot)
    {
        processRequest.Add(/* ... your data object to process. Assumes this is a queue */);
        waitHandle.Set(); // Signal to the thread there is data to process
    }
}

void QueueCometWaitRequest_WaitCallback() 
{    
    while (isRunning)    
    {       
        // Waits here using 0% CPU until the waitHandle.Set is called above
        this.waitHandle.WaitOne();

        // Ensures no-one sets waithandle while data is being processed and
        // subsequently reset
        lock(syncRoot)
        {
            for (int i = 0; i < processRequest.Length; i++)           
            {                        
                // Process the message. 
                // What's the type of processRequest? Im assuming a queue or something     
            }       

            // Reset the Waithandle for the next requestto process
            this.waitHandle.Reset();
        }
    }        
} 

这将确保您的线程在等待时使用0%的CPU,只有在有工作要做时才会消耗CPU。
如果没有,请考虑使用第三方解决方案来实现异步双向消息传递?我曾经在.NET应用程序中成功使用RabbitMQ(AMQP)处理高吞吐量的消息传递。 RabbitMQ的API意味着当接收到消息时,您将获得一个事件,然后可以在后台线程上处理它。
最好的问候,

1
谢谢你的回答。但是又出现了另一个问题:即使在给定的超时期间没有新消息,我也需要向客户端返回响应。在这种情况下,我该如何使用waitHandle.Set();?我的意思是,我怎么知道何时启动线程? - Mehmet
没问题。我在想,你的代码示例不应该导致 CPU 占用率达到 100%。你创建了多少个线程?(猜测一下)应该只有一个!不是每个请求都需要一个线程吧? - Dr. Andrew Burnett-Thompson
2
@Mehmet,我强烈建议你考虑RabbitMQ作为第三方异步消息传递解决方案。这将处理点对点(一个客户端,一个服务器)和多播(服务器到所有客户端)之间的消息管道,具有低CPU使用率和高吞吐量。你可以通过在服务器上实现定时器来向所有客户端实现周期性状态更新,如果没有收到新通知,则向所有客户端发送一条消息。 - Dr. Andrew Burnett-Thompson
再次感谢,我会看一下RabbitMQ,但不幸的是我们的网站已经开放了,我不知道是否可以完全将实现方式改为RabbitMQ。 - Mehmet
@Mehmet 不确定。我能提供的唯一解释是你正在创建超过5个线程,正如你所说的那样。5个使用100毫秒休眠的线程不应该导致CPU飙升!计时器将使用线程池来限制线程的创建。你能发布更多的代码吗?同时尝试我提到的分析工具。不确定它们是否适用于ASP.NET - dotTrace是你最好的选择。这些工具非常棒,可以诊断性能问题。 - Dr. Andrew Burnett-Thompson
显示剩余6条评论

0
我将连接的客户端保存在内存中(字典对象)。
如果静态使用字典对象,则它们不是线程安全的。如果将其用作静态成员,则需要创建一个锁语句。
以下是从Log4Net LoggerFactory类中提取的示例...请注意,TypeToLoggerMap是一个字典对象,当通过GetLogger方法引用它时,会使用Lock语句。
public static class LoggerFactory
{
    public static ILogger GetLogger(Ninject.Activation.IContext context)
    {
        return GetLogger(context.Request.Target == null ? typeof(ILogger) : context.Request.Target.Member.DeclaringType);
    }

    private static readonly Dictionary<Type, ILogger> TypeToLoggerMap = new Dictionary<Type, ILogger>();

    private static ILogger GetLogger(Type type)
    {
        lock (TypeToLoggerMap)
        {
            if (TypeToLoggerMap.ContainsKey(type))
                return TypeToLoggerMap[type];

            ILogger logger = new Logger(type);
            TypeToLoggerMap.Add(type, logger);

            return logger;
        }
    }
}

看看这篇文章 - 这就是我发现关于字典对象的上述信息的地方。

https://www.toptal.com/dot-net/hunting-high-cpu-usage-in-dot-net

顺便提一下,您是否考虑在项目中使用SignalR?


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