简体   繁体   English

多线程c#应用程序占用了很高的CPU使用率

[英]multi-thread c# application hitting high CPU usage

I am working on an application that will connect to x number of hardware devices. 我正在开发将连接到x个硬件设备的应用程序。 I have designed the ReaderLayer such that there is a dedicated thread where the code runs to connect to a single device and continuously read the device data buffer. 我对ReaderLayer进行了设计,以便有一个专用线程,代码在该线程中运行以连接到单个设备并连续读取设备数据缓冲区。 The code for the reader layer is as follows: 阅读器层的代码如下:

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


        }

There is a Logic Layer built on top of the Reader Layer that is responsible for processing the data received from the Readers and forwarding it via HTTP Post. 在阅读器层之上构建了一个逻辑层,该逻辑层负责处理从阅读器接收的数据并通过HTTP Post转发。 It has y number of threads each running a separate Logic Block that has to process relevant information that gets written to its thread-safe queue. 它有y个线程,每个线程运行一个单独的逻辑块,该逻辑块必须处理要写入其线程安全队列的相关信息。 The logic layer subscribes to the ReadForward event that is exposed by the Reader Layer and forwards that data to the relevant logic block by writing to its ConcurrentQueue . 逻辑层订阅Reader层公开的ReadForward事件,并通过写入其ConcurrentQueue将数据转发到相关逻辑块。

The code in each of the Logic Block is pretty straightforward: 每个逻辑块中的代码都非常简单:

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

            }
        }

The layout of the loop is mostly the same for both the Reader Layer and the Logic Layer while(true) . 对于Reader层和Logic层while(true) ,循环的布局几乎相同。 However when I tested with 3 readers and 3 logic blocks, I found that my CPU utilization jumped up to 77%. 但是,当我使用3个读取器和3个逻辑块进行测试时,我发现我的CPU利用率跃升到77%。 I quickly narrowed down the CPU usage to Logic Threads as I received 50% usage for 2 and 25% usage for 1 block. 我迅速将CPU使用范围缩小到逻辑线程,因为我收到2个块的50%的使用率和1个块的25%的使用率。

I am able to bring down the CPU usage to ~3% simply by adding in a Thread.Sleep(100) in the logic thread with 3 blocks, however, I am worried that my logic could be out of synch. 我只需在3个块的逻辑线程中添加Thread.Sleep(100)就能将CPU使用率降低到3%左右,但是,我担心我的逻辑可能不同步。 By looking at the sample could anyone please suggest to me any improvements as the production code will need to work with about 30 logic blocks. 通过查看样本,任何人都可以向我提出任何改进建议,因为生产代码将需要使用大约30个逻辑块。 Do I need to change my architecture? 我需要改变架构吗?

You're doing a whole lot of needless polling in this loop: 您在此循环中进行了很多不必要的轮询:

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

        }
    }

Assuming that the queue is empty most of the time, most of what this code does is repeatedly check the queue. 假设大多数时间队列是空的,那么此代码所做的大部分工作都是重复检查队列。 "Is there anything for me? How 'bout now? Now? ..." “我有东西吗?现在怎么样?现在呢?……”

You can get rid of all that useless polling by replacing your ConcurrentQueue with a BlockingCollection . 您可以通过使用BlockingCollection替换ConcurrentQueue来摆脱所有无用的轮询。 Assuming that you change LogicBuffer to a BlockingCollection , the loop then looks like this: 假设您将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 will dequeue items as they arrive, and will continue to do so until the collection is empty and has been marked as complete for adding. GetConsumingEnumerable会在项目到达时使它们出队,并将继续这样做,直到集合为空并被标记为已添加完成。 See BlockingCollection.CompleteAdding . 请参见BlockingCollection.CompleteAdding The real beauty, though, is that GetConsumingEnumerable does a non-busy wait. 但是,真正的GetConsumingEnumerableGetConsumingEnumerable了不忙的等待。 If the collection is empty, it waits for a notification that an item is available. 如果该集合为空,则它等待项可用的通知。 It doesn't do a lot of useless polling with TryDequeue . 它不会对TryDequeue进行很多无用的轮询。

Changing code that uses ConcurrentQueue so that it uses BlockingCollection instead is pretty easy. 更改使用ConcurrentQueue代码,使其改为使用BlockingCollection非常容易。 You can probably do it in under an hour. 您大概可以在一个小时内完成。 Doing so will make your code a lot simpler, and it will eliminate needless polling. 这样做将使您的代码简单得多,并且将消除不必要的轮询。

Update 更新

If you need to do some periodic processing, you have two choices. 如果需要进行一些定期处理,则有两种选择。 If you want that done in the same loop that's reading the BlockingCollection , then you can change the loop that uses GetConsumingEnumerable to something like: 如果您希望在读取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;
    }
}

That will give you periodic processing rate in the range of 200 to 400 ms. 这将为您提供200到400毫秒范围内的定期处理速率。 It's kind of ugly, in my opinion, and might not be good enough for your purposes. 在我看来,这有点丑陋,可能无法满足您的目的。 You could reduce the timeout to 20 ms instead of 200, which will give you closer to your 200 ms polling rate, at the cost of 10 times as many calls to TryTake . 您可以将超时从20毫秒减少到20毫秒,而不是200毫秒,这将使您更接近200毫秒的轮询速率,而对TryTake调用的代价是10倍。 Likely, you wouldn't notice a difference in CPU usage. 您可能不会注意到CPU使用率的差异。

My preference would be to separate that periodic processing loop from the queue consumer. 我的偏好是将定期处理循环与队列使用者分开。 Create a timer that fires every 200 ms, and have it do the work. 创建一个计时器,每200毫秒触发一次,并使其工作。 For example: 例如:

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
}

That will give you an update frequency of very close to 200 ms. 这将使您的更新频率非常接近200毫秒。 The timer proc will be executed on a separate thread, true, but the only time that thread is active will be when the timer fires. 计时器proc将在单独的线程上执行,为true,但是该线程处于活动状态的唯一时间将是在计时器触发时。

It's simple really, you never yield control, so your code is always executing, so each thread of your code is using 100% of 1 core (so 25% of your cpu if you have 4 cores). 确实很简单,您永远不会屈服于控制,因此您的代码始终在执行,因此代码的每个线程都使用100%的1核(如果使用4核,则使用25%的cpu)。

There's no magic there, Windows doesn't "try to guess" you want to wait and throttle you, you need to explcitely wait, you'd need to show the implementation of ProcessNewTagReceivedLogic but i'm guessing "somewhere" you're making a network connection, instead of polling (just checking and again untill you get non null), you want to call something that actually holds and yields untill it gets an answer. 那里没有魔术,Windows不会“试图猜测”您想要等待并限制您,您需要明确等待,您需要显示ProcessNewTagReceivedLogic的实现,但我猜测您正在“某个地方”网络连接,而不是轮询(只需检查一次,直到得到非null的值),而是要调用实际上持有并产生直到得到答案的东西。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM