BackgroundService 的核心职责是什么
它不是万能的后台线程封装,而是专为
Microsoft.Extensions.Hosting生命周期设计的“托管服务”:启动时执行初始化逻辑、运行中持续处理任务、关闭时支持优雅退出。如果你直接用
Task.Run或裸
Thread,就绕过了主机的生命周期控制,可能导致应用关闭时任务被粗暴中断。
如何正确继承并实现 ExecuteAsync
ExecuteAsync是唯一必须重写的抽象方法,但它**不能阻塞**,也不能只执行一次就返回——必须维持一个长期运行的循环,并响应
CancellationToken。常见错误是写成同步等待或漏掉
await Task.Delay导致 CPU 占满。 使用
while (!stoppingToken.IsCancellationRequested)判断退出时机 每次循环体结尾必须有非忙等挂起(如
await Task.Delay(1000, stoppingToken)) 所有异步操作(如数据库查询、HTTP 调用)都要传入
stoppingToken,否则取消信号无法穿透
public class PollingJobService : BackgroundService
{
private readonly ILogger<PollingJobService> _logger;
public PollingJobService(ILogger<PollingJobService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Executing background job...");
await DoWorkAsync(stoppingToken);
}
catch (OperationCanceledException)
{
// 由 stoppingToken 触发,正常退出路径,无需记录异常
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in background job");
}
// 避免高频轮询;Delay 也需接收 stoppingToken
await Task.Delay(5000, stoppingToken);
}
}
private async Task DoWorkAsync(CancellationToken ct)
{
// 示例:调用带取消支持的 API
await Task.Delay(100, ct); // 模拟工作
}
}
注册时必须用 AddHostedService
不能用
AddSingleton或
AddScoped替代 —— 后台服务的启动/停止顺序、异常捕获、依赖注入上下文生命周期均由
IHostedService契约保障。注册错会导致服务根本不运行,且无任何报错提示。 在
Program.cs中调用
services.AddHostedService<pollingjobservice>()</pollingjobservice>如果服务有构造依赖(如
IHttpClientFactory),确保它们已提前注册 多个
BackgroundService按注册顺序启动,但停止顺序相反(LIFO),注意资源依赖关系
如何安全地触发一次性任务或外部唤醒
BackgroundService本身不提供“手动触发”能力。若需要响应外部事件(如 API 请求、消息队列消息),得引入协调机制,常见做法是结合
Channel<t></t>或
ConcurrentQueue<t></t>+
ManualResetEventSlim。 避免在
ExecuteAsync中直接
await阻塞式队列读取(如
queue.TryDequeue循环),仍需配合
Task.Delay或
Channel.Reader.WaitToReadAsync
Channel<t></t>是推荐方案:支持异步读写、背压、取消传播,且轻量 切勿在
StopAsync中长时间阻塞(如等待队列清空超 5 秒),主机默认只给 5 秒超时,超时后强制终止进程
复杂点往往不在“怎么跑起来”,而在于“怎么停干净”——尤其是涉及未完成 I/O、未释放句柄、或持有静态状态的服务。取消令牌必须贯穿每一层异步调用栈,否则
StopAsync可能永远等不到结束。
