Jeffcky 阅读(39) 评论(0)

前言

这个问题从未遇见过,是一位前辈问我EF Core内存泄漏问题时我才去深入探讨这个问题,刚开始我比较惊讶,居然还有这种问题,然后就有了本文,直接拿前辈的示例代码并稍加修改成就了此文,希望对在自学EF Core过程中的童鞋能有些许帮助。

EntityFramework Core内存泄漏回顾

接下来我将用简单示例代码来还原整个造成EntityFramework Core内存泄漏的过程,同时在这个过程中您也可思考一下其中的原因和最终的结果是否一致。

    public class TestA
    {
        public long Id { get; set; }
        public string Name { get; set; }
    }
    public class EFCoreDbContext : DbContext
    {
        public EFCoreDbContext(DbContextOptions options)
            : base(options)
        {
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=MemoryLeak;integrated security=True;MultipleActiveResultSets=True;");
            base.OnConfiguring(optionsBuilder);
        }

        public DbSet<TestA> TestA { get; set; }
    }
    public class TestUserCase
    {
        public void InvokeMethod(IServiceProvider serviceProvider)
        {
            EFCoreDbContext _context = null;

            if (_context == null)
            {
                _context = serviceProvider.GetRequiredService<EFCoreDbContext>();
            }

            for (var i = 0; i < 10; i++)
            {
                var testA = _context.TestA.FirstOrDefault();
                Console.WriteLine(i);
            }
        }
    }

如上是整个示例代码,重头戏来了,接下来我们在控制台中来通过依赖注入上下文,并获取注入容器中的上下文并调用上述TestUserCase类中的方法,如下:

            var services = new ServiceCollection();

            services.AddDbContext<EFCoreDbContext>(options => options.UseSqlServer("data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=MemoryLeak;integrated security=True;MultipleActiveResultSets=True;"));

            var serviceProvider = services.BuildServiceProvider();

            for (int i = 0; i < 1000; i++)
            {
                var test = new TestUserCase();
                test.InvokeMethod(serviceProvider);
            }

 

通过上述测试内存基本在15兆左右,当然根据机器配置不同最终得到的结果有所差异,但是内存基本没有什么大的波动,接下来我们来改造上述代码。上述我们将serviceProvider通过方法传递到TestUserCase中的InvokeMethod方法中,为了简便我们将获取到的serviceProvider改造成静态的,如下:

    public static class ServiceLocator
    {
        private static IServiceCollection _services;

        public static IServiceProvider Instance
        {
            get
            {
                if (_services == null)
                    return null;
                else
                    return _services.BuildServiceProvider();
            }
        }

        public static void Init(IServiceCollection services)
        {
            _services = services;
        }
    }
    public class TestUserCase
    {
        public void InvokeMethod()
        {
            IServiceScope _serviceScope = null;
            EFCoreDbContext _context = null;
            if (_context == null)
            {
                _serviceScope = ServiceLocator.Instance.GetRequiredService<IServiceScopeFactory>().CreateScope();
                _context = _serviceScope.ServiceProvider.GetRequiredService<EFCoreDbContext>();
            }

            for (var i = 0; i < 10; i++)
            {
                var testA = _context.TestA.FirstOrDefault();
                Console.WriteLine(i);
            }
        }
    }
            var services = new ServiceCollection();

            services.AddDbContext<EFCoreDbContext>(options => options.UseSqlServer("data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=MemoryLeak;integrated security=True;MultipleActiveResultSets=True;"));

            ServiceLocator.Init(services);

            for (int i = 0; i < 1000; i++)
            {
                var test = new TestUserCase();
                test.InvokeMethod();
            }

如上我们通过ServiceLocator类来构建serviceProvider,并将返回serviceProvider赋值给静态变量,然后在我们调用的方法中直接获取容器中的上下文,这样就免去了传递的麻烦。

经过我们上述改造后最终运行内存达到了比较可怕的三百多兆,看来上下文压根就没进行GC,那我是不是改造成如下就可以了呢?

            var scopeFactory = ServiceLocator.Instance.GetRequiredService<IServiceScopeFactory>();

            using (var scope = scopeFactory.CreateScope())
            using (var context = scope.ServiceProvider.GetRequiredService<EFCoreDbContext>())
            {
                for (var i = 0; i < 10; i++)
                {
                    var testA = context.TestA.FirstOrDefault();
                    Console.WriteLine(i);
                }
            }

 

原以为是上下文没有及时得到释放而导致内存激增,但是看到上述结果依然一样没有任何改变,问题是不是到此就结束了呢?下面我们改变注入上下文的方式看看,如下:

            var services = new ServiceCollection();

            var options = new DbContextOptionsBuilder<EFCoreDbContext>()
              .UseSqlServer("data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=MemoryLeak;integrated security=True;MultipleActiveResultSets=True;")
              .Options;
            services.AddScoped(s => new EFCoreDbContext(options));

当我们改变了注入上下文方式后发现此时不会造成内存泄漏,也就是说上下文得到了GC,无论是我是否是手动释放上下文即通过Using包括或者不包括都不会出现内存泄漏问题。通过注入方式不同得到的结果截然不同,但是在我们的理解中通过AddDbContext注入上下文中的第二个参数是默认为Scope,那和我们通过AddScoped注入上下文应该是一样对不对,那为何结果又不同呢?岂不是冲突了吗?在Web不会出现这样的问题,未深入研究,我猜测其原因可能如下:

通过AddDbContext注入上下文只适用于Web应用程序即只对Web应用程序有效而对控制台程序可能无效,同时在Web应用程序中AddDbContext注入上下文和AddScoped注入上下文一致,而对于控制台程序存在不一致问题。一言以蔽之,在Web和Console中通过AddDbContext注入上下文可能存在处理机制不同。

总结

不知如上浅薄分析是否有漏洞或者说代码有错误的地方,期待看到本文的您能有更深入的见解可留下您的评论,如果结果是这样不建议在控制台中使用AddDbContext方法注入上下文。