多线程c#应用程序达到高CPU使用率

时间:2014-10-30 01:51:46

标签: c# .net multithreading

我正在开发一个连接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个逻辑块。我需要改变我的架构吗?

2 个答案:

答案 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;你正在建立一个网络连接,而不是轮询(只是检查并再次直到你得到非空),你想要调用实际上持有的东西并产生直到得到答案。