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

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
解决方案

问题根源

  1. 重复启动导致多线程循环:调用StartServiceAsync时会再次执行StartAsync,原有的循环可能仍在等待计时器信号,未及时响应取消,导致多个循环同时运行。
  2. CancellationTokenSource管理混乱:StartAsync中重新创建cts,手动停止操作时会和Host的取消信号冲突,旧的cts实例未被正确清理。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 18:15:54