ASP.NET Core Web API HostedService中CancellationToken失效问题排查
问题描述
我开发了一个ASP.NET Core 8 Web API,包含HostedService与控制器。控制器通过HostedService实现的IControllableBackgroundService接口,实现服务的启动、停止与重启功能,默认无需人工干预。
StartAsync方法使用PeriodicTimer,每15秒尝试执行一次任务。目前遇到的问题是:调用停止服务的操作后,服务并未停止——定时计时器触发后,仍会启动新的任务周期。CancellationToken本应线程安全,但未检测到取消请求。
代码示例
Program.cs
using HostedService.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddSingleton<IControllableBackgroundService, UserOfficeHostedService>(); builder.Services.AddHostedService<UserOfficeHostedService>(); builder.Services.Configure<HostOptions>(x => { x.ServicesStartConcurrently = true; x.ServicesStopConcurrently = false; }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseCors(x => x .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() ); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
IControllableBackgroundService
namespace HostedService.Services; public interface IControllableBackgroundService { Task StartServiceAsync(); Task StopServiceAsync(); Task RestartServiceAsync(); }
BackgroundServiceController
using HostedService.Services; using Microsoft.AspNetCore.Mvc; namespace HostedService.Controllers; [ApiController] [Route("[controller]")] public class BackgroundServiceController(IControllableBackgroundService backgroundService) : ControllerBase { [HttpGet] [Route("start")] public async Task<IActionResult> StartService() { await backgroundService.StartServiceAsync(); return Ok(); } [HttpGet] [Route("stop")] public async Task<IActionResult> StopService() { await backgroundService.StopServiceAsync(); return Ok(); } [HttpGet] [Route("restart")] public async Task<IActionResult> RestartService() { await backgroundService.RestartServiceAsync(); return Ok(); } }
UserOfficeHostedService
namespace HostedService.Services; public class UserOfficeHostedService( ILogger<UserOfficeHostedService> logger) : IHostedService, IControllableBackgroundService, IDisposable { private CancellationTokenSource cts = new(); private PeriodicTimer timer = new (TimeSpan.FromSeconds(15)); public async Task StartAsync(CancellationToken cancellationToken) { logger.LogInformation("starting service"); cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); while (await timer.WaitForNextTickAsync(cts.Token)) { if (cts.Token.IsCancellationRequested) break; await UpdateUserOfficeCacheAsync(cts.Token); } } public async Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("stopping service"); await cts.CancelAsync(); timer.Dispose(); } public async Task StartServiceAsync() { if (!cts.IsCancellationRequested) return; timer = new PeriodicTimer(TimeSpan.FromSeconds(15)); await StartAsync(cts.Token); } public async Task StopServiceAsync() { await cts.CancelAsync(); await StopAsync(CancellationToken.None); } public async Task RestartServiceAsync() { await StopAsync(cts.Token); timer = new PeriodicTimer(TimeSpan.FromSeconds(15)); await StartAsync(cts.Token); } private async Task UpdateUserOfficeCacheAsync(CancellationToken cancellationToken) { logger.LogInformation($"Performing update on UserOffice Cache. Time: {DateTimeOffset.Now}"); try { if (cancellationToken.IsCancellationRequested) { logger.LogInformation($"Cancellation received before work attempted. Time: {DateTimeOffset.Now}"); return; } // Simulate task await Task.Delay(16000, cancellationToken); // Replace this with actual long-running task logic if (cancellationToken.IsCancellationRequested) { logger.LogInformation($"Cancellation received after work started. Time: {DateTimeOffset.Now}"); } } catch (TaskCanceledException) { logger.LogInformation("UserOffice Cache update was canceled."); } finally { logger.LogInformation("UserOffice Background Service released semaphore."); } } public void Dispose() { timer?.Dispose(); cts?.Dispose(); } }
运行日志
info: HostedService.Services.UserOfficeHostedService[0] starting service info: Microsoft.Hosting.Lifetime[14] Now listening on: https://localhost:7016 info: HostedService.Services.UserOfficeHostedService[0] Performing update on UserOffice Cache. Time: 7/16/2024 4:41:51 PM -04:00 info: HostedService.Services.UserOfficeHostedService[0] stopping service info: HostedService.Services.UserOfficeHostedService[0] UserOffice Background Service released semaphore. info: HostedService.Services.UserOfficeHostedService[0] Performing update on UserOffice Cache. Time: 7/16/2024 4:42:07 PM -04:00
解决方案
问题根源
- 重复启动导致多线程循环:调用
StartServiceAsync时会再次执行StartAsync,原有的循环可能仍在等待计时器信号,未及时响应取消,导致多个循环同时运行。 - CancellationTokenSource管理混乱:
StartAsync中重新创建cts,手动停止操作时会和Host的取消信号冲突,旧的cts实例未被正确清理。 - PeriodicTimer未彻底终止:停止服务后,旧计时器虽被Dispose,但原循环可能因异常处理不当继续执行。
修复代码
重构后的UserOfficeHostedService
namespace HostedService.Services; public class UserOfficeHostedService( ILogger<UserOfficeHostedService> logger) : IHostedService, IControllableBackgroundService, IDisposable { private readonly object _lockObj = new(); private bool _isRunning; private CancellationTokenSource _cts = new(); private PeriodicTimer _timer = new(TimeSpan.FromSeconds(15)); public async Task StartAsync(CancellationToken cancellationToken) { logger.LogInformation("starting service"); lock (_lockObj) { if (_isRunning) return; _isRunning = true; } using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token); try { while (await _timer.WaitForNextTickAsync(linkedCts.Token)) { linkedCts.Token.ThrowIfCancellationRequested(); await UpdateUserOfficeCacheAsync(linkedCts.Token); } } catch (OperationCanceledException) { logger.LogInformation("Service loop was canceled"); } finally { lock (_lockObj) { _isRunning = false; } } } public async Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("stopping service"); await _cts.CancelAsync(); await Task.Delay(100, cancellationToken); _timer.Dispose(); } public async Task StartServiceAsync() { lock (_lockObj) { if (_isRunning) return; // 重置取消令牌和计时器 _cts.Dispose(); _cts = new CancellationTokenSource(); _timer.Dispose(); _timer = new PeriodicTimer(TimeSpan.FromSeconds(15)); } await StartAsync(CancellationToken.None); } public async Task StopServiceAsync() { await _cts.CancelAsync(); // 等待循环彻底退出 while (_isRunning) { await Task.Delay(50); } await StopAsync(CancellationToken.None); } public async Task RestartServiceAsync() { await StopServiceAsync(); await StartServiceAsync(); } private async Task UpdateUserOfficeCacheAsync(CancellationToken cancellationToken) { logger.LogInformation($"Performing update on UserOffice Cache. Time: {DateTimeOffset.Now}"); try { cancellationToken.ThrowIfCancellationRequested(); // 分阶段检测取消,避免长任务无法及时响应 for (int i = 0; i < 16; i++) { await Task.Delay(1000, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); } logger.LogInformation("UserOffice Cache update completed successfully."); } catch (TaskCanceledException) { logger.LogInformation("UserOffice Cache update was canceled."); } finally { logger.LogInformation("UserOffice Background Service released resources."); } } public void Dispose() { _timer?.Dispose(); _cts?.Dispose(); } }
关键修复点
- 线程安全状态控制:使用锁和
_isRunning标记,确保同一时间只有一个循环在运行,避免重复启动。 - CancellationTokenSource生命周期管理:每次启动服务时重置
_cts,避免旧取消信号干扰新循环。 - 彻底停止等待:
StopServiceAsync中等待_isRunning变为false,确保循环完全退出后再执行后续清理。 - 分阶段取消检测:长任务中定期检测取消信号,保证取消请求能及时被响应。
内容的提问来源于stack exchange,提问作者Lee Z
相关产品推荐
相关产品推荐

