后台服务中Timer对象运行异常及依赖注入上下文问题求助
你的核心问题是Timer的生命周期与Scope生命周期不匹配:Timer是长期运行的,但它依赖的IWorkerService是从循环内的短暂Scope中获取的,当Scope被using块自动释放后,IWorkerService及其依赖的上下文(比如DbContext)也被回收,Timer后续触发任务时就会访问已释放的实例,抛出以下异常:
Exception Found: System.InvalidOperationException: Exception in SessionExtendHandle(), Detail : System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection
而把Scope移到循环外长期持有,会导致Scoped服务无法被正常回收,进而引发内存泄漏。
解决方案:让Timer每次执行任务时创建独立Scope
修改TimerObject,不再在构造时注入IWorkerService,而是注入IServiceScopeFactory,每次Timer触发任务时,动态创建新的Scope、获取服务、执行任务,任务完成后自动释放Scope。这样既保证了每次任务都使用有效的上下文,也不会长期持有Scope导致内存泄漏。
1. 重构TimerObject类
public class TimerObject : IDisposable { private readonly IServiceScopeFactory _serviceScopeFactory; private Timer _timer; public TimerObject(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } public void StartTimer() { // 根据实际需求设置Timer的触发间隔 _timer = new Timer(ExecuteTimerTask, null, TimeSpan.Zero, TimeSpan.FromMinutes(5)); } private void ExecuteTimerTask(object state) { // 每次任务执行时创建独立Scope using var scope = _serviceScopeFactory.CreateScope(); var workerService = scope.ServiceProvider.GetRequiredService<IWorkerService>(); try { // 执行你的业务逻辑 workerService.SessionExtendHandle(); } catch (Exception ex) { // 按需处理异常,比如日志记录 Console.WriteLine($"Timer任务执行失败: {ex.Message}"); } } // 实现IDisposable,清理Timer资源 public void Dispose() { _timer?.Dispose(); GC.SuppressFinalize(this); } }
2. 简化BackgroundService代码
不需要在循环内重复创建Scope,只需要初始化一次Timer,然后等待取消信号即可:
public class ServiceWorker : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; private TimerObject _timerObject; public ServiceWorker(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 只初始化一次Timer if (_timerObject == null) { _timerObject = new TimerObject(_serviceScopeFactory); _timerObject.StartTimer(); } // 保持服务运行直到收到取消信号 await Task.Delay(Timeout.Infinite, stoppingToken); } // 重写Dispose,清理Timer资源 public override void Dispose() { _timerObject?.Dispose(); base.Dispose(); } }
方案优势
- 每次Timer任务都使用全新的Scope,避免上下文被提前释放的问题;
- Scope随任务完成自动释放,Scoped服务能被正常回收,不会引发内存泄漏;
- Timer的生命周期与BackgroundService一致,服务停止时能通过Dispose方法正确清理Timer资源。
内容的提问来源于stack exchange,提问作者Carrie Kaski

