【问题标题】:multi-thread c# application hitting high CPU usage多线程 c# 应用程序达到高 CPU 使用率
【发布时间】:2014-10-30 01:51:45
【问题描述】:

我正在开发一个将连接到 x 个硬件设备的应用程序。我已经设计了 ReaderLayer,以便有一个专用线程运行代码以连接到单个设备并连续读取设备数据缓冲区。 reader层的代码如下:

        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));
                }


        }

在 Reader 层之上构建了一个逻辑层,负责处理从 Reader 接收到的数据并通过 HTTP Post 转发。它有 y 个线程,每个线程都运行一个单独的逻辑块,该逻辑块必须处理写入其线程安全队列的相关信息。逻辑层订阅阅读器层公开的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);
                }

            }
        }

读取器层和逻辑层while(true) 的循环布局基本相同。然而,当我使用 3 个阅读器和 3 个逻辑块进行测试时,我发现我的 CPU 利用率跃升至 77%。我很快将 CPU 使用率缩小到逻辑线程,因为我收到了 50% 的 2 使用率和 25% 的使用率 1 块。

我可以通过在具有 3 个块的逻辑线程中添加 Thread.Sleep(100) 将 CPU 使用率降低到 ~3%,但是,我担心我的逻辑可能不同步。通过查看示例,任何人都可以向我提出任何改进建议,因为生产代码需要使用大约 30 个逻辑块。我需要改变我的架构吗?

【问题讨论】:

    标签: c# .net multithreading


    【解决方案1】:

    你在这个循环中做了很多不必要的轮询:

        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 毫秒范围内的周期性处理速率。在我看来,这有点丑陋,可能不足以满足您的目的。您可以将超时时间从 200 减少到 20 毫秒,这将使您更接近 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 将在单独的线程上执行,确实如此,但该线程唯一处于活动状态的时间将是计时器触发时。

    【讨论】:

    • 嗨,Jim,感谢您提出使用 BlockingCollection 的非常有用的建议。这正在帮助我解决当前的问题。然而, ProcessNewTagReceivedLogic() 做了两件事。首先,它使用信息更新私人列表。其次,它试图根据迄今为止收到的信息来维持 TAG 的状态。理想情况下,我希望逻辑的第二部分每 200 毫秒唤醒一次并处理本地列表。如果我把它放在 LogicBuffer.GetConsumingEnumerable() 的 foreach 中,延迟可能超过 1 秒可能不是一个好主意
    • 对于更新,请注意您使用的计时器。 System.Timers.Timer 可以很好地使用它,但是如果您使用了 System.Threading.Timer it could get garbage collected,如果您在程序中的某处没有对它的引用。 (例如,在 ProcessLogicBuffer 末尾的 GC.KeepAlive(timer) 将为发布的示例修复它,如果该示例使用可以被释放的计时器)
    • @ScottChamberlain:感谢您的来信。当我打算使用System.Threading.Timer 时,我搞砸了System.Timers.Timer。我会更正它并添加GC.KeepAlive
    【解决方案2】:

    真的很简单,你永远不会放弃控制,所以你的代码总是在执行,所以你的代码的每个线程都使用 100% 的 1 个内核(如果你有 4 个内核,则使用 25% 的 cpu)。

    那里没有魔法,Windows 不会“试图猜测”你想要等待并限制你,你需要明确等待,你需要展示 ProcessNewTagReceivedLogic 的实现,但我猜你“在某个地方” '正在建立网络连接,而不是轮询(只是检查并再次直到你得到非空),你想要调用一些实际持有并产生直到它得到答案的东西。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多