C# Timer报错:无法访问已释放的IServiceProvider对象原因排查
问题原因与解决方案
核心原因
服务生命周期不匹配
如果你的EmailVerificationService被注册为Scoped或Transient类型,当它所在的作用域结束后,DI容器会回收该服务实例,导致注入的IServiceProvider被释放。但Timer基于线程池运行,会持续持有对服务实例的引用,第二次触发回调时就会访问已被释放的IServiceProvider,抛出异常。async void回调的风险CheckVerificationExpiration使用了async void,这种异步方法的异常无法被上层捕获,会直接导致进程崩溃。更关键的是,async void会让方法执行脱离当前实例的生命周期管理,即便实例已被回收,回调仍可能继续执行。未实现资源清理
你的服务没有实现IDisposable接口来停止Timer,导致服务实例被回收后,Timer依然在后台运行,持续尝试访问已释放的成员。
解决方案
1. 将服务注册为Singleton
因为服务需要长期运行Timer,必须保证实例不会被DI容器提前回收。在Program.cs中注册:
builder.Services.AddSingleton<EmailVerificationService>();
2. 实现IDisposable清理Timer
添加资源清理逻辑,确保服务被回收时停止Timer,避免无效回调。
3. 替换async void为async Task并捕获异常
修改回调方法为async Task,配合try/catch捕获异常,防止进程崩溃,同时确保异步操作正确完成。
修改后的完整代码
public class EmailVerificationService : IDisposable { private Timer? _timer; private string? _email; private readonly IServiceProvider _serviceProvider; private bool _disposed = false; public EmailVerificationService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public void Start(string email) { _email = email; // 用lambda包装async Task回调 _timer = new Timer(async _ => await CheckVerificationExpiration(), null, TimeSpan.Zero, TimeSpan.FromSeconds(3)); } private async Task CheckVerificationExpiration() { // 已释放或无邮箱时直接返回 if (_disposed || string.IsNullOrEmpty(_email)) return; try { var now = DateTime.UtcNow; using var scope = _serviceProvider.CreateScope(); await using var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var user = await dbContext.Users .Where(u => u.Email == _email && now >= u.ConfirmCodeExpires) .FirstOrDefaultAsync(); if (user != null) { dbContext.Users.Remove(user); await dbContext.SaveChangesAsync(); } } catch (Exception ex) { // 此处添加日志记录,避免异常逃逸 // 例如:_logger.LogError(ex, "检查邮箱验证过期状态时发生错误"); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // 停止Timer并释放资源 _timer?.Change(Timeout.Infinite, Timeout.Infinite); _timer?.Dispose(); } _disposed = true; } }
关键注意点
- 根
IServiceProvider(Singleton注入的)不会被DI容器释放,所以在回调中创建作用域是安全的。 - 使用
await using确保AppDbContext被正确释放。 - 所有异步操作必须用
await等待,避免异步操作未完成就结束方法。
内容的提问来源于stack exchange,提问作者Revan Zakaryali
相关产品推荐
相关产品推荐

