.Net Core Web API中Fire-and-forget实现方案与问题探讨
.NET Core 3.1 Web API 火并遗忘任务相关问题
我们有一个.NET Core 3.1 Web API,需要在请求处理流程的后期、服务层深处完成一个**Fire-and-forget(火并遗忘)**任务。
声明:我知晓当前方案并非最佳实践,但团队已接受该方案及其风险。
当前简化实现
public class MyService { public readonly IServiceA _serviceA; public readonly IServiceB _serviceB; public MyService(IServiceA serviceA, IServiceB serviceB) { _serviceA = serviceA; _serviceB = serviceB; } public void FireTheTask(MyData data) { _ = Task.Run(() => Submit(data)); } public async Task Submit(MyData data) { try { var result = await _serviceA.CallSomeService(data); // 使用HttpClientFactory if(!result.Success) { _serviceB.SaveData(data); // 用于离线处理 } } catch (Exception ex) { // 在此处将错误记录到文件 } } }
ServiceA和ServiceB均注册为transient并通过依赖注入注入。理论上存在主请求先于Submit()完成,导致ServiceA和ServiceB不可用的风险,但实际观察发现即使在Submit()开头添加await Task.Delay(30000),仍能在主请求完成30秒后成功执行。
1. 如何解释该观察结果?
这是因为:
- .NET Core请求作用域释放后,transient服务实例本身不会被立即销毁,只要还没有被GC(垃圾回收器)回收,就能继续执行方法。30秒的延迟内,这些实例刚好没被GC清理,所以能正常工作。
- 另外
ServiceA依赖的HttpClient是由HttpClientFactory池化管理的,即使ServiceA实例被回收,池中的HttpClient可能仍处于可用状态,也会让请求暂时成功。
但这种情况是不可靠的,GC的回收时机完全不确定,一旦实例被回收,后续操作就会抛出异常。
2. 若当前方案存在不可靠风险,以下改进方案是否可行(假设服务无需引用主请求作用域内的对象)?
先看改进后的代码:
public class MyService { public readonly IServiceScopeFactory _scopeFactory; public MyService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public void FireTheTask(MyData data) { _ = Task.Run(() => Submit(data)); } public async Task Submit(MyData data) { try { using var scope = _scopeFactory.CreateScope(); var serviceA = scope.ServiceProvider.GetRequiredService<IServiceA>(); var serviceB = scope.ServiceProvider.GetRequiredService<IServiceB>(); var result = await serviceA.CallSomeService(data); // 使用HttpClientFactory if(!result.Success) { serviceB.SaveData(data); // 用于离线处理 } } catch (Exception ex) { // 在此处将错误记录到文件 } } }
这个方案完全可行:
- 通过
IServiceScopeFactory创建的是独立于主请求的新作用域,这个作用域不会随主请求结束而释放,能自主管理内部服务的生命周期。 - 在该作用域内获取的
ServiceA和ServiceB属于这个独立作用域,会在using块结束时被正确释放,彻底避免了主请求作用域释放带来的服务不可用风险。 - 因为假设不需要主请求作用域内的对象,所以这个方案完全适配你的场景。
3. 该方案能否处理ServiceA和ServiceB所依赖的transient服务?
可以处理。
- 当从独立作用域的
ServiceProvider获取ServiceA和ServiceB时,它们依赖的transient服务也会从当前作用域的服务提供者获取,这些依赖的transient服务同样属于这个独立作用域,生命周期由该作用域管理,不会受主请求作用域的影响,能被正确创建和释放。
4. 该方案能否进一步改进或简化?若可以,具体如何操作?
可以从以下几点优化:
简化与性能优化
- 去掉
Task.Run:Submit本身是异步方法,直接调用_ = Submit(data).ConfigureAwait(false);即可,不需要额外开启线程池线程,减少不必要的线程切换开销。
日志规范优化
- 注入
ILogger<MyService>替代自定义文件日志:符合.NET Core的日志生态,能统一管理日志输出(控制台、文件、第三方平台等),代码更规范。
代码简化
- 可以用泛型解析服务,但当前写法已经足够清晰,若想简化可直接在作用域内解析,无需额外变量(不过可读性会稍降):
var result = await scope.ServiceProvider.GetRequiredService<IServiceA>().CallSomeService(data);
可靠性增强(可选)
- 如果团队后续愿意调整,可改用
BackgroundService或自定义后台任务队列来处理这类火并遗忘任务,这是.NET Core官方推荐的后台任务处理方式,比直接开异步任务更可靠,能避免应用池回收等问题,但如果坚持当前方案,前面的优化已经足够。
内容的提问来源于stack exchange,提问作者pikkabird
相关产品推荐
相关产品推荐

