You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 12:11:04