【问题标题】:Why is an object created by GetService not being destructed?为什么 GetService 创建的对象没有被破坏?
【发布时间】:2020-04-21 16:14:19
【问题描述】:

我正在编写一个针对 dotnet 核心框架 3.1 的应用程序。我使用依赖注入来配置数据库上下文等。在我的 Program.cs 中,我有以下代码:

var host = new HostBuilder()
    .ConfigureHostConfiguration(cfgHost =>
    {
        ...
    })
    .ConfigureAppConfiguration((hostContext, configApp) =>
    {
        ....
    })
    .ConfigureServices((hostContext, services) =>
    {
        ...
        services.AddDbContext<MyHomeContext>(options =>
        {
            options.UseNpgsql(hostContext.Configuration.GetConnectionString("DbContext"));
        }, ServiceLifetime.Transient);
        ...
    })
    .ConfigureLogging((hostContext, logging) =>
    {
        ...    
    })
    .Build();

我将host 传递给另一个班级。在另一个类中,作为更长方法的一部分,我有以下代码:

    using (var context = Host.Services.GetService(typeof(MyHomeContext)) as MyHomeContext)
    {
        StatusValues = context.Status.ToDictionary(kvp => kvp.Name, kvp => kvp.Id);
    }
    GC.Collect();
    GC.Collect();

GC.Collect 电话用于测试/调查目的。在MyHomeContext 中,出于测试目的,我实现了析构函数和 Dispose() 的覆盖。 Dispose() 被调用,但析构函数永远不会被调用。 这会导致我创建的每个MyHomeContext 实例都会发生内存泄漏。

我错过了什么?我该怎么做才能确保MyHomeContext 的实例在我不再需要时被删除。

出于以下几个原因,我转向了这个工具:

  • 我只需要很短时间的数据库连接。
  • 我插入了大量数据(不是在上面简化的示例/测试代码中),导致 DbContext 保持很大的缓存。我希望处理对象会释放内存,但现在我只会让它变得更糟:(

当我用new MyHomeContext() 替换Host.Services.GetService(typeof(MyHomeContext)) as MyHomeContext 时,正在调用MyHomeContext 的析构函数。在我看来,依赖注入框架中的某些东西持有对该对象的引用。这是真的?如果是这样,我该如何告诉它释放它?

【问题讨论】:

  • 不是这样的。您不应该依赖于被调用的析构函数。在Dispose 中释放您的托管和非托管资源。
  • 嗨 Fildor,感谢您的帮助。我认为你没有很好地阅读这个问题。问题是/是该对象在使用 GetService 创建后没有被销毁。
  • 其实没有。我确实读得足够好。接受的答案更详细地重申了我写的内容。您刚刚遇到了另一个导致您观察到的行为的问题,答案中也提到了这个问题,但我的评论中没有提到。我知道会有另一个因素,因此我没有写答案,而是发表评论。

标签: c# dependency-injection memory-leaks microsoft.extensions.hosting


【解决方案1】:

很难对你的问题给出一个好的答案,因为有很多误解需要解决。以下是一些需要注意的事项:

  • 在调试器中运行的非优化(调试构建).NET 应用程序的行为与未连接调试器的优化应用程序完全不同。一方面,在调试时,方法的所有变量将始终保持引用。这意味着对GC.Collect() 的任何调用都将无法清理由同一方法引用的context 变量。
  • Dispose Pattern 被正确实现时,当调用它的Dispose 方法时,对终结器的调用将被类抑制。这是通过调用GC.SuppressFinalize 来完成的。 Entity Framework 的 DbContext 正确实现了 Dispose 模式,这也会导致您看不到终结器被命中。
  • 终结器(析构函数)在称为finalizer thread 的后台线程上调用。这意味着即使您的context 被取消引用并且有资格进行垃圾回收,也不太可能在调用GC.Collect() 之后立即调用终结器。但是,您可以暂停您的应用程序并等待通过调用GC.WaitForPendingFinalizers() 调用终结器。调用 WaitForPendingFinalizers 几乎不是您在生产环境中想要做的事情,但它对于测试和基准测试很有用。

除了这些 CLR 特定部分,这里有一些关于 DI 部分的反馈:

  • 从 DI 容器解析的服务不应直接释放。相反,由于 DI 容器可以控制其创建,因此您也应该让它控制其销毁。
  • 执行此操作(使用 MS.DI)的方法是创建一个IServiceScope。服务被缓存在这样的范围内,当范围被释放时,它将确保其缓存的一次性服务也被释放,并确保以相反的创建顺序完成。
  • 直接从根容器(在您的情况下为Host.Services)请求服务是一个坏主意,因为它会导致作用域服务(例如您的DbContext)被缓存在根容器中。这导致它们有效地成为单例。换句话说,相同的DbContext 实例将在应用程序期间重复使用,无论您多久从Host.Services 请求它。这可能导致all sorts of hard to debug problems。再次,解决方案是创建一个范围并从该范围解析。例子:
    var factory = Host.Services.GetRequiredService<IServiceScopeFactory>();
    using (var scope = factory.CreateScope())
    {
        var service = scope.ServiceProvider.GetRequiredService<ISomeService>();
        service.DoYourMagic();
    }
    
  • 请注意,使用 ASP.NET Core 时,您通常不必手动创建新范围 - 每个 Web 请求都会自动获得自己的范围。您的所有类都会自动从一个范围请求,并且该范围会在网络请求结束时自动清理。

【讨论】:

  • 嗨史蒂文,我将您的详细回复标记为答案。代码块中的部分确实解决了它。你在上面的一些言论与我的观察不符。一些反馈:未优化(调试......:如我的问题中所述,用“新”实现替换上下文的创建,导致调用析构函数/终结器。当 Dispose 模式为 ...:可能是. 但是,使用范围实现(您提供的解决方案),正在调用上下文终结器。我只是用 Console.WriteLine 实现它以进行测试。
  • 嗨@jokr,如果您在调用Dispose() 后看到终结器被击中,那么您可能正在使用Entity Framework Core,因为它是doesn't suppress finalization。另一方面,实体框架 6,does suppress finalization
猜你喜欢
  • 1970-01-01
  • 2022-11-18
  • 2020-03-05
  • 2014-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-09
相关资源
最近更新 更多