在 BeginLifetimeScope 配置中使用 RegisterInstance 导致内存泄漏

Using RegisterInstance in BeginLifetimeScope configuration causes memory leak

我对 Autofac 的用法如下

public class Executor 
{
  private readonly ILifetimeScope rootScope;

  public Executor(ILifetimeScope rootScope)
  {
     this.rootScope = rootScope;
  }

  public async Task ExecuteAsync(IData data)
  {
    using (ILifetimeScope childScope = this.rootScope.BeginLifetimeScope(builder =>
    {
      var module = new ExecutionModule(data);
      builder.RegisterModule(module);
    }))
    {
        // resolve services that depend on IData and do the work
    }
  }
}

public class ExecutionModule : Module
{
  private readonly IData data;

  public ExecutionModule(IData data)
  {
    this.data = data;
  }

  protected override void Load(ContainerBuilder builder)
  {
    builder.RegisterInstance(this.data).As<IData>();
  }
}

ExecuteAsync 在应用程序运行时被调用很多次,IData 表示应该与内部服务共享的外部输入(每个 ExecuteAsync 各不相同)生命周期范围。

应用 运行 一段时间后,我开始遇到内存耗尽的问题。后来通过额外的分析我确定 IData 的实例在垃圾收集中幸存下来并且似乎导致了内存泄漏。

作为实验,ExecutionModule(也就是 RegisterInstance)的注册已从代码中删除,内存泄漏已消除

Autofac 的代码有有趣的 remarks for BeginLifetimeScope:

/// The components registered in the sub-scope will be treated as though they were
/// registered in the root scope, i.e., SingleInstance() components will live as long
/// as the root scope.

问题是:如果RegisterInstance意味着注册单例,那么在这种情况下,IData的实例将在根范围内存在,即使它们可以只能在注册的 childScope 内解析,对吗?

我认为该评论可能需要重新审视和修正。自从将 lot 添加到 ILifetimeScope 界面以来,lot 发生了变化(看起来像六年?),而 internals 发生了变化,接口没有,所以文档还没有被重新访问。 I've added an issue to get those docs updated.

这是我 运行 使用 Autofac 6 的快速测试:

using System;
using Autofac;
using Autofac.Diagnostics;

namespace AutofacDemo
{
    public static class Program
    {
        public static void Main()
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<Dependency>().SingleInstance();
            using var container = builder.Build();

            var dep = container.Resolve<Dependency>();
            Console.WriteLine("Dependency registered as single instance in root scope:");
            Console.WriteLine("ID: {0}", dep.Id);
            Console.WriteLine("Scope tag: {0}", dep.ScopeTag);
            Console.WriteLine();

            var rootScopeTag = dep.ScopeTag;

            using var standaloneScope = container.BeginLifetimeScope();
            dep = standaloneScope.Resolve<Dependency>();
            Console.WriteLine("Dependency registered in root, resolved in child scope:");
            Console.WriteLine("ID: {0}", dep.Id);
            Console.WriteLine("Scope tag: {0}", dep.ScopeTag);
            Console.WriteLine("Resolved from root? {0}", dep.ScopeTag == rootScopeTag);
            Console.WriteLine();

            using var singleInstanceScope = container.BeginLifetimeScope(
                b => b.RegisterType<Dependency>()
                    .SingleInstance());
            dep = singleInstanceScope.Resolve<Dependency>();
            Console.WriteLine("Dependency registered as single instance in child scope:");
            Console.WriteLine("ID: {0}", dep.Id);
            Console.WriteLine("Scope tag: {0}", dep.ScopeTag);
            Console.WriteLine("Resolved from root? {0}", dep.ScopeTag == rootScopeTag);
            Console.WriteLine();

            var instance = new Dependency();
            using var registerInstanceScope = container.BeginLifetimeScope(
                b => b.RegisterInstance(instance)
                    .OnActivating(e => e.Instance.ScopeTag = e.Context.Resolve<ILifetimeScope>().Tag));
            dep = registerInstanceScope.Resolve<Dependency>();
            Console.WriteLine("Dependency registered as instance in child scope:");
            Console.WriteLine("ID: {0}", dep.Id);
            Console.WriteLine("Scope tag: {0}", dep.ScopeTag);
            Console.WriteLine("Resolved from root? {0}", dep.ScopeTag == rootScopeTag);
        }
    }

    public class Dependency
    {
        public Dependency()
        {
            this.Id = Guid.NewGuid();
        }

        public Dependency(ILifetimeScope scope)
            : this()
        {
            this.ScopeTag = scope.Tag;
        }

        public Guid Id { get; }

        public object ScopeTag { get; set; }
    }
}

它所做的是尝试一些方法来解析 .SingleInstance().RegisterInstance<T>() 组件并显示它们实际来自哪个范围。它通过将 ILifetimeScope 注入构造函数来实现这一点——您将始终获得解析组件的生命周期范围。控制台输出如下所示:

Dependency registered as single instance in root scope:
ID: 056e1584-fa2a-4657-b58c-ccc8bfc504d2
Scope tag: root

Dependency registered in root, resolved in child scope:
ID: 056e1584-fa2a-4657-b58c-ccc8bfc504d2
Scope tag: root
Resolved from root? True

Dependency registered as single instance in child scope:
ID: 3b410502-b6f2-4670-a182-6b3eab3d5807
Scope tag: System.Object
Resolved from root? False

Dependency registered as instance in child scope:
ID: 20028683-23c1-48a0-adbe-94c3a8180ed7
Scope tag: System.Object
Resolved from root? False

.SingleInstance() 生命周期范围项将始终从它们注册的生命周期范围内解析。这会阻止您创建一个奇怪的 captive dependency 问题。您会在前两个解析操作中注意到这一点 - 从根容器解析 嵌套范围显示范围标记是 root 范围标记,所以我们知道它是在这两种情况下都来自容器。

RegisterInstance<T> 没有依赖关系,因为它是构造的,但我通过添加 OnActivating 处理程序来模拟功能。在这种情况下,我们看到对象实际上是从子生命周期范围(一种空的 System.Object 标记)而不是根解析的。

您将在 Autofac 源代码中看到的是,任何时候您 BeginLifetimeScope()creates a "scope restricted registry" 这是在子作用域中注册的组件的存储位置。当处理范围时,它会被处理。

虽然我不能保证运行你所看到的不是问题,但我的经验是,将代码简化为 Whosebug 问题可以隐藏一些真实问题的复杂性真正重要的世界体系。例如:

  • 如果 IData 是一次性的,它将一直保留到子生命周期范围被丢弃;如果没有调用子范围的处置,那就是泄漏。
  • 我见过有人不小心在 BeginLifetimeScope lambda 中使用了 original ContainerBuilder,这会导致混乱和问题。
  • 如果线程或执行模型有更复杂的东西可以在处理发生之前停止处理或中止异步线程,那可能会很麻烦。

不是说你有任何这些问题。我 am 说鉴于上面的代码还不足以实际重现问题,所以 可能 泄漏是更大的东西的症状被省略了。

如果是我,我会:

  • 确保我使用的是最新的 Autofac,如果我还没有的话。
  • 看看在模块中注册数据对象 而不是 是否有所不同。 应该没关系,但是...值得一试。
  • 真正深入探查器结果以查看保存数据对象的内容。它是根容器吗?还是每个子范围?是否有什么东西保留在这些子作用域上而不让它们被处置或清理?

而且,可能相关的是,我们即将发布一个版本,其中包含对 ConcurrentBag usage on some platforms 的一些修复,这可能会有所帮助。那应该是 6.2.0,我希望今天能把它弄出来。