简体   繁体   English

阻塞集合<T>在 BackgroundService 中导致高 CPU 使用率

[英]BlockingCollection<T> in a BackgroundService causes high CPU usage

I have a .NET BackgroundService for managing notifications by using a BlockingCollection<Notification> .我有一个 .NET BackgroundService用于使用BlockingCollection<Notification>管理通知。

My implementation is cause high CPU usage, even though there is not that much work to be handled by the BlockingCollection .我的实现导致 CPU 使用率高,即使BlockingCollection没有那么多工作要处理。

I've collected some dumps and it seems that I am running into thread pool starvation.我收集了一些转储,似乎我遇到了线程池饥饿问题。

I am not sure how this should be refactor to avoid this situation.我不确定应该如何重构以避免这种情况。

private readonly BlockingCollection<Notification> _notifications;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Task.Run(async () =>
        {
            await _notificationsContext.Database.MigrateAsync(stoppingToken);

            while (!stoppingToken.IsCancellationRequested)
            {

                foreach (var notification in _notifications.GetConsumingEnumerable(stoppingToken))
                {
                   // process notification
                }


            }
        }, stoppingToken);
    }

I've also tried to remove the while loop but the issue still persists.我也尝试删除 while 循环,但问题仍然存在。

核心转储截图

EDIT: Added the producer编辑:添加了制作人

 public abstract class CommandHandlerBase
    {
        private readonly BlockingCollection<Notification> _notifications;

        public CommandHandlerBase(BlockingCollection<Notification> notifications)
        {
            _notifications = notifications;
        }
        protected void EnqueueNotification(AlertImapact alertImapact,
                                           AlertUrgency alertUrgency,
                                           AlertSeverity alertServerity,
                                           string accountName,
                                           string summary,
                                           string details,
                                           bool isEnabled,
                                           Exception exception,
                                           CancellationToken cancellationToken = default)
        {

            var notification = new Notification(accountName, summary, details, DateTime.UtcNow, exception.GetType().ToString())
            {
                Imapact = alertImapact,
                Urgency = alertUrgency,
                Severity = alertServerity,
                IsSilenced = !isEnabled,
            };

            _notifications.Add(notification, cancellationToken);
        }
    }

Blocking is expensive but putting the thread to sleep and rescheduling is even more expensive.阻塞是昂贵的,但让线程休眠和重新调度更昂贵。 To avoid this .NET typically starts blocking operations with a SpinWait before actually blocking the thread.为了避免这种情况,.NET 通常在实际阻塞线程之前使用SpinWait开始阻塞操作。 A spinwait uses a core to do nothing for a while, which causes the CPU usage you observed. Spinwait 使用一个核心暂时不做任何事情,这会导致您观察到的 CPU 使用率。

To fix this, use an asynchronous collection like Channels .要解决此问题,请使用像Channels这样的异步集合。

  • A channel allows you to asynchronously post or read messages to it, preserving their order.通道允许您向其异步发布或读取消息,并保留其顺序。
  • It's thread safe which means multiple readers and writers can write to it at the same time.它是线程安全的,这意味着多个读者和作者可以同时写入它。
  • You can create a bounded channel to prevent publishers to post if a channel is full.您可以创建一个有界频道,以防止发布者在频道已满时发帖。
  • Finally, you can read all messages in a Channel through an IAsyncEnumerable , making the processing code easier.最后,您可以通过IAsyncEnumerable读取 Channel 中的所有消息,使处理代码更容易。

Avoid blocking with Channels避免使用 Channels 阻塞

In your case, the code could change to this:在您的情况下,代码可能会更改为:

private readonly Channel<Notification> _notifications=Channel.CreateUnbounded<Notification>();

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    await _notificationsContext.Database.MigrateAsync(stoppingToken);

    await foreach(var notification in _notifications.Reader.ReadAllAsync(stoppingToken))
    {
               // process notification
    }
}

Channels intentionally use separate interfaces for reading and writing.通道有意使用单独的接口进行读取和写入。 To Read, you use the ChannelReader class returned by Channel.Reader .阅读,您使用ChannelReader通过返回的类Channel.Reader To write, you use the ChannelWriter class returned by Channel.Writer .写,您使用ChannelWriter通过返回的类Channel.Writer A Channel can be implicitly cast to either type, making it easy to write publisher and subscriber methods that only accept/produce a ChannelReader or ChannelWriter. Channel 可以隐式转换为任一类型,从而可以轻松编写仅接受/生成 ChannelReader 或 ChannelWriter 的发布者和订阅者方法。

To write to the channel you use ChannelWriter's WriteAsync method:要写入通道,请使用 ChannelWriter 的WriteAsync方法:

await _notifications.Writer.WriteAsync(someNotification);

When you're done writing and want to close the channel, you need to call Complete() on the writer:当您完成编写并想要关闭通道时,您需要在编写器上调用Complete()

await _notification.Writer.Complete();

The processing loop will read any remaining messages.处理循环将读取任何剩余的消息。 To await until it finishes you need to await the ChannelReader.Completion task:要等到它完成,您需要等待ChannelReader.Completion任务:

await _notification.Reader.Completion;

Posting from other classes从其他班级发帖

When you work with a BackgroundService notifications will typically arrive from other classes.当您使用 BackgroundService 时,通知通常来自其他类。 This means that somehow both the publisher and the service need access to the same Channel.这意味着以某种方式发布者和服务都需要访问同一个频道。 One way to do this is to use a helper class and inject it both in the publisher and service.一种方法是使用辅助类并将其注入发布者和服务中。

The MessageChannel<T> class does this and also handles application termination by closing the writer: MessageChannel<T>类执行此操作,并通过关闭编写器来处理应用程序终止:

public class MessageChannel<T>:IDisposable 
    {
        private readonly Channel<Envelope<T>> _channel;

        public ChannelReader<Envelope<T>> Reader => _channel;
        public ChannelWriter<Envelope<T>> Writer => _channel;

        public MessageChannel(IHostApplicationLifetime lifetime)
        {
            _channel = Channel.CreateBounded<Envelope<T>>(1);
            lifetime.ApplicationStopping.Register(() => Writer.TryComplete());
        }

        private readonly CancellationTokenSource _cts = new();

        public CancellationToken CancellationToken => _cts.Token;
        public void Stop()
        {
            _cts.Cancel();
        }

        public void Dispose()
        {
            _cts.Dispose();
        }
    }

This can be injected in the background service:这可以在后台服务中注入:

MessageChannel<Notification> _notifications;
ChannelReader<Notification> _reader;

public MyService(MessageChannel<Notification> notifications)
{
    _notifications=notifications;
    _reader=notifications.Reader;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    await _notificationsContext.Database.MigrateAsync(stoppingToken);

    await foreach(var notification in _reader.ReadAllAsync(stoppingToken))
    {
               // process notification
    }
}

事实证明,该问题与另一个BackgroundService相关,该服务正在等待错误计算的TimeSpan导致线程池饥饿。

While I think there can be arguments towards the proposed channel solution, like proposed earlier, I would vote for a more simple solution, the channels are intended for a high volume of messages if You will, so do consider it if there are very many messages.虽然我认为对于提议的渠道解决方案可能存在争议,就像之前提出的那样,我会投票支持更简单的解决方案,如果您愿意,渠道旨在处理大量消息,所以如果有很多消息,请考虑一下.

I Suspect your high CPU happen because your notification queue is empty and You have no waits.我怀疑你的 CPU 过高是因为你的通知队列是空的,你没有等待。

public class Worker : BackgroundService
    {
        private readonly ConcurrentQueue _messages = new ConcurrentQueue();               

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await Task.Factory.StartNew(() =>
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    await _notificationsContext.Database.MigrateAsync(stoppingToken);
                    while (_messages.TryDequeue(out var notification) && !stoppingToken.IsCancellationRequested)
                    {
                        //ProcessNotificaiton      
                    }
                    
                    //Explicit delay for the cases when you have no notification you do not want to enter frantic looping which is what i suspect is happening
                   Task.Delay(1000, stoppingToken).GetAwaiter().GetResult();
                }
            });
        }
    }

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

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