我正在开发一个连接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转发。它有多个线程,每个线程运行一个单独的逻辑块,必须处理写入其线程安全队列的相关信息。逻辑层订阅由Reader Layer公开的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 Layer和Logic Layer while(true)
,循环的布局大致相同。然而,当我测试3个读卡器和3个逻辑块时,我发现我的CPU利用率上升到77%。我很快将CPU使用率缩小到了Logic Threads,因为我获得了2%的使用率和1%的25%的使用率。
我可以通过在逻辑线程中添加3个块的Thread.Sleep(100)来将CPU使用率降低到~3%,但是,我担心我的逻辑可能不同步。通过查看样本,任何人都可以向我建议任何改进,因为生产代码需要使用大约30个逻辑块。我需要改变我的架构吗?
答案 0 :(得分:5)
你在这个循环中做了很多不必要的轮询:
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);
}
}
}
假设大多数时候队列都是空的,这段代码的大部分内容都是重复检查队列。 “我有什么事吗?现在怎么回事?现在?......”
您可以将ConcurrentQueue
替换为BlockingCollection来摆脱所有无用的民意调查。假设您将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
做了非繁忙的等待。如果集合为空,则等待项目可用的通知。它不会对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毫秒而不是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,但是线程处于活动状态的唯一时间是定时器触发时。
答案 1 :(得分:0)
很简单,你永远不会控制,所以你的代码总是在执行,所以你的代码的每个线程都使用100%的1核(如果你有4个核,那么你的cpu的25%)。
那里没有魔力,Windows没有尝试猜测&#34;你想等待并扼杀你,你需要等待,你需要展示ProcessNewTagReceivedLogic的实现,但我猜测某些地方&#34;你正在建立一个网络连接,而不是轮询(只是检查并再次直到你得到非空),你想要调用实际上持有的东西并产生直到得到答案。