[英]multi-thread c# application hitting high CPU usage
我正在开发将连接到x个硬件设备的应用程序。 我对ReaderLayer进行了设计,以便有一个专用线程,代码在该线程中运行以连接到单个设备并连续读取设备数据缓冲区。 阅读器层的代码如下:
while (true)
{
// read buffer from the reader
IList<IRampActiveTag> rampTagList = ConnectedReader.ReadBuffer();
if (rampTagList != null && rampTagList.Any())
{
// trigger read event handler
if (ReadMessage != null)
ReadMessage(this, new RampTagReadEventArg(rampTagList));
}
}
在阅读器层之上构建了一个逻辑层,该逻辑层负责处理从阅读器接收的数据并通过HTTP Post转发。 它有y个线程,每个线程运行一个单独的逻辑块,该逻辑块必须处理要写入其线程安全队列的相关信息。 逻辑层订阅Reader层公开的ReadForward
事件,并通过写入其ConcurrentQueue
将数据转发到相关逻辑块。
每个逻辑块中的代码都非常简单:
public void ProcessLogicBuffer()
{
while (true)
{
// Dequeue the list
IRampActiveTag tag;
LogicBuffer.TryDequeue(out tag);
if (tag != null)
{
ProcessNewTagReceivedLogic(tag);
Console.WriteLine("Process Buffer #Tag {0}. Buffer Count #{1}", tag.NewLoopId, LogicBuffer.Count);
}
}
}
对于Reader层和Logic层while(true)
,循环的布局几乎相同。 但是,当我使用3个读取器和3个逻辑块进行测试时,我发现我的CPU利用率跃升到77%。 我迅速将CPU使用范围缩小到逻辑线程,因为我收到2个块的50%的使用率和1个块的25%的使用率。
我只需在3个块的逻辑线程中添加Thread.Sleep(100)就能将CPU使用率降低到3%左右,但是,我担心我的逻辑可能不同步。 通过查看样本,任何人都可以向我提出任何改进建议,因为生产代码将需要使用大约30个逻辑块。 我需要改变架构吗?
您在此循环中进行了很多不必要的轮询:
public void ProcessLogicBuffer()
{
while (true)
{
// Dequeue the list
IRampActiveTag tag;
LogicBuffer.TryDequeue(out tag);
if (tag != null)
{
ProcessNewTagReceivedLogic(tag);
Console.WriteLine("Process Buffer #Tag {0}. Buffer Count #{1}", tag.NewLoopId, LogicBuffer.Count);
}
}
}
假设大多数时间队列是空的,那么此代码所做的大部分工作都是重复检查队列。 “我有东西吗?现在怎么样?现在呢?……”
您可以通过使用BlockingCollection替换ConcurrentQueue
来摆脱所有无用的轮询。 假设您将LogicBuffer
更改为BlockingCollection
,则循环如下所示:
public void ProcessLogicBuffer()
{
foreach (var tag in LogicBuffer.GetConsumingEnumerable())
{
ProcessNewTagReceivedLogic(tag);
Console.WriteLine("Process Buffer #Tag {0}. Buffer Count #{1}", tag.NewLoopId, LogicBuffer.Count);
}
}
GetConsumingEnumerable会在项目到达时使它们出队,并将继续这样做,直到集合为空并被标记为已添加完成。 请参见BlockingCollection.CompleteAdding 。 但是,真正的GetConsumingEnumerable
是GetConsumingEnumerable
了不忙的等待。 如果该集合为空,则它等待项可用的通知。 它不会对TryDequeue
进行很多无用的轮询。
更改使用ConcurrentQueue
代码,使其改为使用BlockingCollection
非常容易。 您大概可以在一个小时内完成。 这样做将使您的代码简单得多,并且将消除不必要的轮询。
如果需要进行一些定期处理,则有两种选择。 如果您希望在读取BlockingCollection
的同一循环中完成此操作,则可以将使用GetConsumingEnumerable
的循环更改为:
Stopwatch sw = Stopwatch.StartNew();
TimeSpan lastProcessTime = TimeSpan.Zero;
while (true)
{
IRampActiveTag tag;
// wait up to 200 ms to dequeue an item.
if (LogicBuffer.TryTake(out tag, 200))
{
// process here
}
// see if it's been 200 ms or more
if ((sw.ElapsedMilliseconds - lastProcessTime.TotalMilliseconds) > 200)
{
// do periodic processing
lastProcessTime = sw.Elapsed;
}
}
这将为您提供200到400毫秒范围内的定期处理速率。 在我看来,这有点丑陋,可能无法满足您的目的。 您可以将超时从20毫秒减少到20毫秒,而不是200毫秒,这将使您更接近200毫秒的轮询速率,而对TryTake
调用的代价是10倍。 您可能不会注意到CPU使用率的差异。
我的偏好是将定期处理循环与队列使用者分开。 创建一个计时器,每200毫秒触发一次,并使其工作。 例如:
public void ProcessLogicBuffer()
{
var timer = new System.Threading.Timer(MyTimerProc, null, 200, 200);
// queue processing stuff here
// Just to make sure that the timer isn't garbage collected. . .
GC.KeepAlive(timer);
}
private void MyTimerProc(object state)
{
// do periodic processing here
}
这将使您的更新频率非常接近200毫秒。 计时器proc将在单独的线程上执行,为true,但是该线程处于活动状态的唯一时间将是在计时器触发时。
确实很简单,您永远不会屈服于控制,因此您的代码始终在执行,因此代码的每个线程都使用100%的1核(如果使用4核,则使用25%的cpu)。
那里没有魔术,Windows不会“试图猜测”您想要等待并限制您,您需要明确等待,您需要显示ProcessNewTagReceivedLogic的实现,但我猜测您正在“某个地方”网络连接,而不是轮询(只需检查一次,直到得到非null的值),而是要调用实际上持有并产生直到得到答案的东西。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.