如何在调用GetAllUsers控制器时并行运行后台服务
实现控制器与后台服务并行运行的方案
先指出你原代码里的两个问题:
- StopAsync方法自己调用自己,属于无限递归,直接会造成栈溢出,得先删掉这个逻辑。
- 当前代码里的
await Task.Delay(10秒)是同步等待的,控制器会卡10秒才返回结果,完全没实现并行的需求。
下面给你两种可行的实现方案:
方案一:轻量快速实现(适合简单场景)
把后台要执行的逻辑抽成独立方法,在控制器里直接启动任务但不等待它完成,控制器立马返回结果。注意一定要给后台任务加异常捕获,避免没处理的异常搞崩整个应用。
修改后的控制器代码:
private readonly ILogger<UsersController> _logger; public UsersController(ILogger<UsersController> logger) { _logger = logger; } [HttpGet] public async Task<IActionResult> GetAllUsers(CancellationToken stoppingToken) { // 启动后台任务,不等待完成,用下划线忽略返回的Task _ = RunBackgroundTask(stoppingToken); // 直接准备并返回用户数据,不用等后台任务 _logger.LogInformation("开始返回用户数据"); List<UserDto> users = listUsers; return new OkObjectResult(users); } // 单独封装后台任务的逻辑 private async Task RunBackgroundTask(CancellationToken cancellationToken) { try { _logger.LogInformation("后台服务启动于: {time}", DateTimeOffset.Now); await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); _logger.LogInformation("执行用户数据处理逻辑"); // 这里替换成你实际要执行的后台业务代码,比如数据同步、日志分析等 _logger.LogInformation("后台服务结束于: {time}", DateTimeOffset.Now); } catch (OperationCanceledException) { _logger.LogInformation("后台服务被取消"); } catch (Exception ex) { _logger.LogError(ex, "后台服务执行出错"); } }
方案二:队列式后台服务(适合生产环境)
如果你的后台任务比较重要,怕因为应用池回收导致任务丢失,推荐用这种基于队列的后台服务方案,它通过ASP.NET Core的IHostedService来管理任务,可靠性更高。
步骤1:创建任务队列
// 定义队列接口 public interface IBackgroundTaskQueue { void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem); Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken); } // 队列实现 public class BackgroundTaskQueue : IBackgroundTaskQueue { private readonly ConcurrentQueue<Func<CancellationToken, Task>> _workItems = new(); private readonly SemaphoreSlim _signal = new(0); public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem) { if (workItem == null) throw new ArgumentNullException(nameof(workItem)); _workItems.Enqueue(workItem); _signal.Release(); } public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken) { await _signal.WaitAsync(cancellationToken); _workItems.TryDequeue(out var workItem); return workItem; } }
步骤2:创建后台服务类
public class QueuedHostedService : BackgroundService { private readonly ILogger<QueuedHostedService> _logger; private readonly IBackgroundTaskQueue _taskQueue; public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger) { _taskQueue = taskQueue; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("队列后台服务启动"); while (!stoppingToken.IsCancellationRequested) { var workItem = await _taskQueue.DequeueAsync(stoppingToken); try { await workItem(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "执行后台任务时出错"); } } _logger.LogInformation("队列后台服务停止"); } }
步骤3:注册服务
在Program.cs里添加以下代码,把队列和后台服务注入到DI容器:
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>(); builder.Services.AddHostedService<QueuedHostedService>();
步骤4:修改控制器代码
private readonly ILogger<UsersController> _logger; private readonly IBackgroundTaskQueue _taskQueue; // 构造函数注入任务队列 public UsersController(ILogger<UsersController> logger, IBackgroundTaskQueue taskQueue) { _logger = logger; _taskQueue = taskQueue; } [HttpGet] public async Task<IActionResult> GetAllUsers(CancellationToken stoppingToken) { // 把后台任务加入队列,由后台服务自动处理 _taskQueue.QueueBackgroundWorkItem(async token => { _logger.LogInformation("后台服务启动于: {time}", DateTimeOffset.Now); await Task.Delay(TimeSpan.FromSeconds(10), token); _logger.LogInformation("执行用户数据处理逻辑"); // 这里替换成你的实际后台业务代码 _logger.LogInformation("后台服务结束于: {time}", DateTimeOffset.Now); }); // 立即返回用户数据 _logger.LogInformation("开始返回用户数据"); List<UserDto> users = listUsers; return new OkObjectResult(users); }
这种方案的好处是,后台任务会被队列管理,就算控制器已经返回结果,后台服务也会持续处理队列里的任务,还能避免因为应用池回收导致任务丢失(如果需要更持久化,还可以结合数据库做任务持久化)。
内容的提问来源于stack exchange,提问作者Jeya21
相关产品推荐
相关产品推荐

