Microsoft.Extensions.DependencyInjection中的Transient依赖注入关系,使用不当会造成内存泄漏
Microsoft.Extensions.DependencyInjection中(下面简称DI)的Transient依赖注入关系,表示每次DI获取一个全新的注入对象。但是使用Transient依赖注入关系时,最好要配合IServiceScope来一起使用,因为通过Transient依赖注入关系创建的对象,都会被创建它的ServiceProvider对象内部引用,这样会造成注入对象无法被GC及时回收,造成内存泄漏,只有当调用ServiceProvider对象的Dispose方法后,ServiceProvider才会解除其内部对注入对象的引用,之后这些注入对象才能被GC回收。
我们新建一个.NET Core控制台项目,然后假设我们有接口IPeople和实现类People,他们之间的依赖注入关系是Transient。
现在,如果我们的代码中有一个for循环,它会循环1000次,每一次都会从DI中获取一个IPeople对象实例,由于接口IPeople和类People是Transient关系,所以每次DI都会创建一个新的People对象实例。但是我们只需要在每次循环中,调用People类的DoSomething方法做一些事情后,就不需要创建的People对象了,也就是说我们希望每次循环结束后,GC都能尽量回收在循环中创建的People对象实例。
所以我们写了下面的代码:
using Microsoft.Extensions.DependencyInjection; using System; namespace NetCoreDITransientInScope { interface IPeople { void DoSomething(); } class People : IPeople { public void DoSomething() { Console.WriteLine("DoSomething is running"); } } class Program { static void Main(string[] args) { IServiceCollection services = new ServiceCollection(); services.AddTransient<IPeople, People>();//注册接口IPeople和类People的关系为Transient using (ServiceProvider rootServiceProvider = services.BuildServiceProvider()) { //执行1000次循环,每一次循环创建一个People对象实例,在rootServiceProvider调用Dispose方法前,创建的1000个People对象实例都不会被GC回收 for (int i = 0; i < 1000; i++) { IPeople people = rootServiceProvider.GetService<IPeople>(); people.DoSomething(); //在每次循环结束后,创建的People对象实例无法被GC回收,因为在rootServiceProvider的内部对所有创建的Transient对象都保持了引用,除非调用rootServiceProvider的Dispose方法,否则在每次循环中创建的People对象实例都无法被GC回收 } } Console.WriteLine("Press any key to end..."); Console.ReadKey(); } } }
从上面代码的注释中,我们可以看到,实际上每一次for循环执行完后,GC并不能立即回收在循环中创建的People对象实例,原因是ServiceProvider对象rootServiceProvider的内部引用了由DI创建的所有People对象实例,除非调用rootServiceProvider的Dispose方法(也就是在上面using代码块最后),否则所有的People对象实例都无法被GC回收。设想一下,如果将上面的for循环改为一个死循环(对于有些后台服务程序而言,的确需要死循环),那么DI会创建大量的People对象实例无法被GC及时回收,造成内存泄漏。
所以正确使用Transient依赖注入关系的方法应该是,配合IServiceScope对象来使用,我们将上面的代码改为如下:
using Microsoft.Extensions.DependencyInjection; using System; namespace NetCoreDITransientInScope { interface IPeople { void DoSomething(); } class People : IPeople { public void DoSomething() { Console.WriteLine("DoSomething is running"); } } class Program { static void Main(string[] args) { IServiceCollection services = new ServiceCollection(); services.AddTransient<IPeople, People>();//注册接口IPeople和类People的关系为Transient using (ServiceProvider rootServiceProvider = services.BuildServiceProvider()) { //执行1000次循环,每一次循环创建一个People对象实例 for (int i = 0; i < 1000; i++) { //在每一次循环中创建一个IServiceScope对象serviceScope,然后使用serviceScope的ServiceProvider创建People对象实例,这样在rootServiceProvider中并没有对People对象实例的引用,只有每次循环中创建的serviceScope中的ServiceProvider保持了People对象实例的引用,这样在每次循环中当调用serviceScope的Dispose方法后,单次循环中创建的People对象实例就可以被GC回收了,而不是等到rootServiceProvider调用Dispose方法后,才能被GC回收 using (IServiceScope serviceScope = rootServiceProvider.CreateScope()) { IPeople people = serviceScope.ServiceProvider.GetService<IPeople>(); people.DoSomething(); } } } Console.WriteLine("Press any key to end..."); Console.ReadKey(); } } }
从上面代码中,我们可以看到,由于现在在每次for循环中,是由一个独立的IServiceScope对象serviceScope的ServiceProvider,来创建People对象实例,所以在for循环外面的ServiceProvider对象rootServiceProvider,其并没有内部引用由DI创建的People对象实例。而在每次for循环中,我们都调用了serviceScope的Dispose方法(也就是在上面第二个using代码块最后),这样每次循环结束后,就没有任何代码引用循环内的People对象实例了,GC就可以及时回收由DI创建的People对象实例。
在使用Microsoft.Extensions.DependencyInjection的Transient依赖注入关系时,一定要注意本文所述的内存泄漏问题,这个问题可能很多才开始接触Microsoft.Extensions.DependencyInjection的开发人员不会注意到,但是它会严重影响你的程序性能和稳定性。可以参考下面两篇帖子中发帖人提出的问题:
IServiceProvider garbage collection / disposal
When are .NET Core dependency injected instances disposed?
也可以参考在GitHub上,微软官方对这个问题的讨论:
Revisit tracking transient services for disposal
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
2018-10-21 ASP.NET Core 如何设置发布环境
2018-10-21 C# 中的#if、#elif、#else、#endif等条件编译符号 (转载)
2018-10-21 C#中如果用await关键字来await一个为null的Task对象会抛出异常