【问题标题】:BlockingCollection<T> in a BackgroundService causes high CPU usageBackgroundService 中的 BlockingCollection<T> 导致 CPU 使用率高
【发布时间】:2021-12-03 13:39:34
【问题描述】:

我有一个 .NET BackgroundService 用于使用 BlockingCollection&lt;Notification&gt; 管理通知。

我的实现导致 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);
        }
    }

【问题讨论】:

  • 我将从完全删除 Task.Run(async () =&gt; 部分开始。虽然也不需要循环,但它无论如何都不会做任何事情。
  • 您的错误消息显示您正在阻止 GetConsumingEnumerable 调用上的线程池线程,这是阻塞的。如果这导致线程池饥饿,我假设您必须并行调用ExecuteAsync 多次?永远窃取 1 个线程池线程是不好的(这不是线程池的用途),但要导致饥饿,您必须阻塞许多线程池线程。
  • ExecuteAsync 应该返回一个代表后台服务生命周期的任务,所以如果你必须使用它,至少等待你的 Task.Run..
  • 我根本没有调用ExecuteAsync ,我正在使用.NET 辅助服务,运行时正在启动它。我也尝试通过删除Task.Run() 调用,但仍然没有改善。
  • 为什么不在其中放置一些日志记录/断点,看看它被调用了多少次?

标签: c# blockingcollection .net-6.0 worker-service


【解决方案1】:

阻塞是昂贵的,但让线程休眠并重新调度更昂贵。为了避免这种情况,.NET 通常在实际阻塞线程之前使用 SpinWait 开始阻塞操作。 spinwait 使用一个核心在一段时间内什么都不做,这会导致您观察到的 CPU 使用率。

要解决此问题,请使用 Channels 之类的异步集合。

  • 通道允许您以异步方式向其发布或读取消息,同时保留它们的顺序。
  • 它是线程安全的,这意味着多个读取器和写入器可以同时对其进行写入。
  • 您可以创建有界频道,以防止发布者在频道已满时发帖。
  • 最后,您可以通过IAsyncEnumerable 读取Channel 中的所有消息,使处理代码更容易。

避免通道阻塞

在您的情况下,代码可能会更改为:

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

频道有意使用单独的接口进行读取和写入。要阅读,请使用 Channel.Reader 返回的 ChannelReader 类。要编写代码,请使用 Channel.Writer 返回的 ChannelWriter 类。 Channel 可以是implicitly cast 任何一种类型,这使得编写仅接受/产生 ChannelReader 或 ChannelWriter 的发布者和订阅者方法变得容易。

要写入频道,请使用 ChannelWriter 的 WriteAsync 方法:

await _notifications.Writer.WriteAsync(someNotification);

写完想要关闭频道,需要在作者上拨打Complete()

await _notification.Writer.Complete();

处理循环将读取任何剩余的消息。要等待它完成,您需要等待 ChannelReader.Completion 任务:

await _notification.Reader.Completion;

其他班级发帖

当您使用 BackgroundService 时,通知通常会来自其他类。这意味着发布者和服务都需要以某种方式访问​​同一频道。一种方法是使用辅助类并将其注入到发布者和服务中。

MessageChannel&lt;T&gt; 类执行此操作并通过关闭编写器来处理应用程序终止:

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

【讨论】:

  • 感谢您的解决方案,虽然我发现我的问题与代码的另一部分有关,但我会接受您的解决方案,因为它似乎是一个更好的方法。
【解决方案2】:

虽然我认为对于提议的渠道解决方案可能存在争议,就像之前提出的那样,但我会投票支持更简单的解决方案,如果您愿意,渠道是用于大量消息的,所以如果有的话,请考虑非常多的消息。

我怀疑你的 CPU 过高是因为你的通知队列是空的,而且你没有等待。

公共类工人:BackgroundService { 私有只读 ConcurrentQueue _messages = new ConcurrentQueue(); 受保护的覆盖异步任务 ExecuteAsync(CancellationToken stoppingToken) { 等待 Task.Factory.StartNew(() => { 而(!stoppingToken.IsCancellationRequested) { 等待 _notificationsContext.Database.MigrateAsync(stoppingToken); while (_messages.TryDequeue(out var notification) && !stoppingToken.IsCancellationRequested) { //进程通知 } //当你没有通知你不想进入疯狂循环的情况下的显式延迟,这是我怀疑正在发生的事情 Task.Delay(1000,stoppingToken).GetAwaiter().GetResult(); } }); } }

【讨论】:

    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-09
      • 2017-10-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多