簡體   English   中英

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

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

我有一個 .NET BackgroundService用於使用BlockingCollection<Notification>管理通知。

我的實現導致 CPU 使用率高,即使BlockingCollection沒有那么多工作要處理。

我收集了一些轉儲,似乎我遇到了線程池飢餓問題。

我不確定應該如何重構以避免這種情況。

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

我也嘗試刪除 while 循環,但問題仍然存在。

核心轉儲截圖

編輯:添加了制作人

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

阻塞是昂貴的,但讓線程休眠和重新調度更昂貴。 為了避免這種情況,.NET 通常在實際阻塞線程之前使用SpinWait開始阻塞操作。 Spinwait 使用一個核心暫時不做任何事情,這會導致您觀察到的 CPU 使用率。

要解決此問題,請使用像Channels這樣的異步集合。

  • 通道允許您向其異步發布或讀取消息,並保留其順序。
  • 它是線程安全的,這意味着多個讀者和作者可以同時寫入它。
  • 您可以創建一個有界頻道,以防止發布者在頻道已滿時發帖。
  • 最后,您可以通過IAsyncEnumerable讀取 Channel 中的所有消息,使處理代碼更容易。

避免使用 Channels 阻塞

在您的情況下,代碼可能會更改為:

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

通道有意使用單獨的接口進行讀取和寫入。 閱讀,您使用ChannelReader通過返回的類Channel.Reader 寫,您使用ChannelWriter通過返回的類Channel.Writer Channel 可以隱式轉換為任一類型,從而可以輕松編寫僅接受/生成 ChannelReader 或 ChannelWriter 的發布者和訂閱者方法。

要寫入通道,請使用 ChannelWriter 的WriteAsync方法:

await _notifications.Writer.WriteAsync(someNotification);

當您完成編寫並想要關閉通道時,您需要在編寫器上調用Complete()

await _notification.Writer.Complete();

處理循環將讀取任何剩余的消息。 要等到它完成,您需要等待ChannelReader.Completion任務:

await _notification.Reader.Completion;

從其他班級發帖

當您使用 BackgroundService 時,通知通常來自其他類。 這意味着以某種方式發布者和服務都需要訪問同一個頻道。 一種方法是使用輔助類並將其注入發布者和服務中。

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

這可以在后台服務中注入:

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導致線程池飢餓。

雖然我認為對於提議的渠道解決方案可能存在爭議,就像之前提出的那樣,我會投票支持更簡單的解決方案,如果您願意,渠道旨在處理大量消息,所以如果有很多消息,請考慮一下.

我懷疑你的 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