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

如何从Web请求执行耗时任务以避免超时并保持站点响应?

ASP.NET Core 从Web请求触发耗时后台任务的实现方案

核心思路

不用局限于计时器型的Hosted Service,我们可以实现基于队列的后台任务处理机制:Web请求只负责把任务加入队列,立即返回响应避免超时;后台服务持续监听队列,异步执行耗时任务,同时支持任务状态查询。


1. 定义任务模型与队列接口

首先创建存储任务信息的模型,以及线程安全的任务队列接口:

// 任务状态枚举
public enum TaskStatus
{
    Pending,
    Running,
    Completed,
    Failed
}

// 任务模型,包含ID、状态和要调用的API信息
public class BackgroundTaskItem
{
    public Guid TaskId { get; set; }
    public TaskStatus Status { get; set; } = TaskStatus.Pending;
    public string TargetApiUrl { get; set; }
    // 可添加其他业务参数
}

// 任务队列接口,用于生产/消费任务
public interface IBackgroundTaskQueue
{
    void EnqueueTask(BackgroundTaskItem task);
    Task<BackgroundTaskItem> DequeueTaskAsync(CancellationToken cancellationToken);
}

2. 实现线程安全的任务队列

用ConcurrentQueue和SemaphoreSlim实现线程安全的队列,保证多线程场景下的可靠性:

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly ConcurrentQueue<BackgroundTaskItem> _taskQueue;
    private readonly SemaphoreSlim _signal;

    public BackgroundTaskQueue()
    {
        _taskQueue = new ConcurrentQueue<BackgroundTaskItem>();
        _signal = new SemaphoreSlim(0);
    }

    public void EnqueueTask(BackgroundTaskItem task)
    {
        if (task == null) throw new ArgumentNullException(nameof(task));
        
        _taskQueue.Enqueue(task);
        _signal.Release(); // 释放信号通知后台服务有新任务
    }

    public async Task<BackgroundTaskItem> DequeueTaskAsync(CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken); // 等待新任务信号
        _taskQueue.TryDequeue(out var task);
        return task;
    }
}

3. 实现后台任务处理服务

继承BackgroundService,监听队列并执行耗时任务,同时维护任务状态:

public class TaskProcessingHostedService : BackgroundService
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<TaskProcessingHostedService> _logger;
    // 内存存储任务状态,生产环境建议替换为数据库/Redis
    private readonly ConcurrentDictionary<Guid, BackgroundTaskItem> _taskStatusStore = new();

    public TaskProcessingHostedService(IBackgroundTaskQueue taskQueue,
                                       IHttpClientFactory httpClientFactory,
                                       ILogger<TaskProcessingHostedService> logger)
    {
        _taskQueue = taskQueue;
        _httpClientFactory = httpClientFactory;
        _logger = logger;
    }

    // 提供外部查询任务状态的方法
    public BackgroundTaskItem GetTaskStatus(Guid taskId)
    {
        _taskStatusStore.TryGetValue(taskId, out var status);
        return status;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("后台任务处理服务已启动");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                var taskItem = await _taskQueue.DequeueTaskAsync(stoppingToken);
                
                _logger.LogInformation("开始处理任务: {TaskId}", taskItem.TaskId);
                _taskStatusStore.TryAdd(taskItem.TaskId, taskItem);
                taskItem.Status = TaskStatus.Running;

                // 调用目标REST API执行耗时操作
                var httpClient = _httpClientFactory.CreateClient();
                var response = await httpClient.PostAsync(taskItem.TargetApiUrl, null, stoppingToken);
                response.EnsureSuccessStatusCode();

                taskItem.Status = TaskStatus.Completed;
                _logger.LogInformation("任务处理完成: {TaskId}", taskItem.TaskId);
            }
            catch (OperationCanceledException)
            {
                // 服务停止时忽略该异常
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "处理后台任务时发生错误");
                // 捕获任务ID并标记为失败
                if (ex.Data.Contains("TaskId") && Guid.TryParse(ex.Data["TaskId"].ToString(), out var taskId))
                {
                    if (_taskStatusStore.TryGetValue(taskId, out var failedTask))
                    {
                        failedTask.Status = TaskStatus.Failed;
                    }
                }
            }
        }

        _logger.LogInformation("后台任务处理服务已停止");
    }
}

4. 注册服务到依赖注入容器

在Program.cs中注册队列、后台服务和HttpClient工厂:

var builder = WebApplication.CreateBuilder(args);

// 注册单例任务队列
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
// 注册后台任务处理服务
builder.Services.AddHostedService<TaskProcessingHostedService>();
// 注册HttpClient工厂,用于调用外部API
builder.Services.AddHttpClient();

// 注册控制器等其他服务
builder.Services.AddControllersWithViews();

var app = builder.Build();

// 配置中间件
app.UseRouting();
app.MapControllers();

app.Run();

5. 编写Web接口,触发任务并查询状态

创建控制器处理用户请求,快速返回响应并支持任务状态查询:

[ApiController]
[Route("api/tasks")]
public class TaskController : ControllerBase
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly TaskProcessingHostedService _taskService;

    public TaskController(IBackgroundTaskQueue taskQueue, TaskProcessingHostedService taskService)
    {
        _taskQueue = taskQueue;
        _taskService = taskService;
    }

    [HttpPost("start")]
    public IActionResult StartLongRunningTask([FromBody] TaskStartRequest request)
    {
        var taskId = Guid.NewGuid();
        var taskItem = new BackgroundTaskItem
        {
            TaskId = taskId,
            TargetApiUrl = request.TargetApiUrl
            // 填充其他业务参数
        };

        _taskQueue.EnqueueTask(taskItem);
        // 立即返回任务ID,无需等待任务完成
        return Ok(new { TaskId = taskId, Message = "任务已启动,可通过任务ID查询执行状态" });
    }

    [HttpGet("status/{taskId}")]
    public IActionResult GetTaskStatus(Guid taskId)
    {
        var taskStatus = _taskService.GetTaskStatus(taskId);
        if (taskStatus == null)
        {
            return NotFound("指定任务不存在");
        }

        return Ok(new { TaskId = taskStatus.TaskId, Status = taskStatus.Status.ToString() });
    }
}

// 任务启动请求模型
public class TaskStartRequest
{
    public string TargetApiUrl { get; set; }
    // 可添加其他业务参数
}

使用说明

  1. 用户调用POST /api/tasks/start接口提交任务,接口会立即返回任务ID,完全避免Web请求超时。
  2. 用户可以通过GET /api/tasks/status/{taskId}接口轮询任务状态(因为无法用AJAX,用户可以手动刷新页面或定时请求该接口)。
  3. 后台服务会自动从队列中取出任务,异步调用指定的REST API,执行完成后更新任务状态。

生产环境优化建议

  • 任务状态持久化:示例用内存字典存储状态,重启应用会丢失,生产环境建议用SQL Server、Redis等持久化存储。
  • 重试机制:给REST API调用添加重试逻辑,处理临时网络故障。
  • 并发控制:在后台服务中限制同时执行的任务数量,避免资源耗尽。

内容的提问来源于stack exchange,提问作者Jonathan Wood

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 14:55:15