ASP.NET BackgroundService 如何确保仅串行运行?避免重复启动任务
如何确保BackgroundService同一时刻仅运行一个实例?
我有一个从API Controller启动的BackgroundService,要求同一时刻至多运行一个实例。请问如何检查是否已有任务在运行,从而避免启动新的任务?
启动新任务的API及相关代码
[HttpPost] public async Task<IActionResult> RunJob(JobMessage msg) { if (_queue.Count > 0) { return StatusCode(429, "DocumentDistributor are running. Try again later"); } await _queue.Queue(msg); return Ok("DocumentDistributor will start in about one minute."); } public interface IBackgroundTaskQueue { Task Queue(JobMessage message); Task<JobMessage> Dequeue(); public int Count { get; } } public sealed class QueuedHostedService : BackgroundService { private readonly IServiceProvider _serviceProvider; public QueuedHostedService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { using var scope = _serviceProvider.CreateScope(); var calculator = scope.ServiceProvider.GetRequiredService<QueueDocumentDistributor>(); await calculator.RunService(); } catch (OperationCanceledException) { // Prevent throwing if the Delay is cancelled } catch (Exception e) { Log.Error(e, "Error in QueuedHostedService"); } // check queue every 1 minute await Task.Delay(1000 * 60, stoppingToken); } } } public class QueueDocumentDistributor { private readonly IBackgroundTaskQueue _queue; private readonly ReportService _service; public QueueDocumentDistributor(IBackgroundTaskQueue queue, ReportService service) { _queue = queue; _service = service; } public async Task RunService() { var message = await _queue.Dequeue(); if (message == null) return; await _service.CreateReports(message); } }
问题分析
当前代码通过_queue.Count > 0判断是否有任务待处理,但存在关键漏洞:当任务已经从队列取出并正在执行时,队列计数会归零,此时API会允许新任务入队,最终导致多个任务同时运行,不符合"同一时刻至多一个实例"的要求。
解决方案:添加线程安全的运行状态标记
需要一个独立的、线程安全的运行状态标记,结合队列状态双重判断,确保不会重复启动任务。以下是具体实现步骤:
1. 创建单例的任务状态管理类
用SemaphoreSlim实现线程安全的运行状态控制,它既可以标记任务是否在运行,也能强制保证同一时刻只有一个任务执行:
public class JobExecutionStatus { private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // 判断是否有任务正在运行 public bool IsRunning => _semaphore.CurrentCount == 0; // 获取执行权限(无可用权限时阻塞等待) public async Task WaitAsync(CancellationToken cancellationToken = default) { await _semaphore.WaitAsync(cancellationToken); } // 释放执行权限,允许后续任务执行 public void Release() { _semaphore.Release(); } }
2. 注册为单例服务
在Program.cs或Startup.cs中将状态类注册为单例,确保全局共享同一状态:
services.AddSingleton<JobExecutionStatus>();
3. 修改任务执行类,加入状态控制
在QueueDocumentDistributor中注入状态类,在任务执行前后获取/释放权限,确保同一时刻只有一个任务在运行:
public class QueueDocumentDistributor { private readonly IBackgroundTaskQueue _queue; private readonly ReportService _service; private readonly JobExecutionStatus _jobStatus; public QueueDocumentDistributor(IBackgroundTaskQueue queue, ReportService service, JobExecutionStatus jobStatus) { _queue = queue; _service = service; _jobStatus = jobStatus; } public async Task RunService() { var message = await _queue.Dequeue(); if (message == null) return; try { // 获取执行权限,确保同一时刻只有一个任务运行 await _jobStatus.WaitAsync(); await _service.CreateReports(message); } finally { // 无论任务成功或失败,都释放权限,避免死锁 _jobStatus.Release(); } } }
4. 修改API接口,同时检查队列和运行状态
更新RunJob方法,同时判断队列是否有等待任务、是否有任务正在运行,拒绝新的重复请求:
[HttpPost] public async Task<IActionResult> RunJob(JobMessage msg) { // 队列有等待任务 或 已有任务在运行,都返回429拒绝请求 if (_jobStatus.IsRunning || _queue.Count > 0) { return StatusCode(429, "DocumentDistributor正在运行,请稍后重试"); } await _queue.Queue(msg); return Ok("DocumentDistributor将在约一分钟后启动"); }
额外优化建议
- 如果希望任务处理完一个后立即处理下一个,而非等待一分钟,可以移除
QueuedHostedService中的await Task.Delay(1000 * 60, stoppingToken);,改为用Channel实现队列(ChannelReader.ReadAsync会自动等待新任务入队),避免无效的空循环。 - 确保
IBackgroundTaskQueue的Dequeue方法在队列空时不会返回null,而是异步等待新任务,减少不必要的资源消耗。
内容的提问来源于stack exchange,提问作者Martin Andersen
相关产品推荐
相关产品推荐

