在.NET BackgroundService中使用BlockingCollection<T>导致高CPU占用如何解决?
问题根因分析
- 未正确await内部任务:
ExecuteAsync作为BackgroundService的核心执行方法,你没有等待内部Task.Run创建的任务,会导致ExecuteAsync立刻返回,框架会误认为服务执行逻辑已结束,同时内部任务抛出的异常也无法被捕获,容易出现无感知的任务崩溃。 - 同步阻塞浪费线程资源:
BlockingCollection.GetConsumingEnumerable是同步阻塞API,等待队列新元素时会持续占用当前线程池线程不释放,闲置等待场景下会大量浪费线程池资源,高负载下很容易触发线程池饥饿,线程频繁切换就会导致无任务时CPU占用仍过高。 - 冗余循环无实际作用:
GetConsumingEnumerable在未收到取消信号、队列未调用CompleteAdding的情况下不会退出遍历,外层的while循环没有实际作用,就算移除也解决不了核心的同步阻塞问题。
重构方案
推荐使用.NET原生的System.Threading.Channels替代BlockingCollection,它是专门为异步生产者消费者场景设计的,支持异步等待不阻塞线程,从根源避免线程池占用问题。
1. 替换队列实现
首先将注入的BlockingCollection<Notification>替换为Channel<Notification>,初始化时可以根据需求设置容量边界:
// 服务注册时初始化Channel,示例为有界通道,最多存储1000条通知 services.AddSingleton(Channel.CreateBounded<Notification>(1000));
2. 重构生产者代码
public abstract class CommandHandlerBase { private readonly ChannelWriter<Notification> _notificationWriter; public CommandHandlerBase(Channel<Notification> notificationChannel) { _notificationWriter = notificationChannel.Writer; } 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, }; // 同步写入队列,无等待开销 _notificationWriter.TryWrite(notification); } }
3. 重构BackgroundService消费者代码
private readonly ChannelReader<Notification> _notificationReader; private readonly NotificationsContext _notificationsContext; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 直接await执行数据库迁移,不需要套Task.Run await _notificationsContext.Database.MigrateAsync(stoppingToken); // 异步遍历队列元素,无元素时会释放线程等待,不占用线程资源 await foreach (var notification in _notificationReader.ReadAllAsync(stoppingToken)) { // 处理通知逻辑 } }
旧版.NET兼容方案
如果你的项目版本低于.NET 5,不支持IAsyncEnumerable的ReadAllAsync方法,可以用下面的写法兼容:
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _notificationsContext.Database.MigrateAsync(stoppingToken); while (!stoppingToken.IsCancellationRequested) { var notification = await _notificationReader.ReadAsync(stoppingToken); // 处理通知逻辑 } }
内容的提问来源于stack exchange,提问作者Mihaimyh
相关产品推荐
相关产品推荐

