You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Autofac单实例引发Parallel.ForEach下ReportBase集合线程安全问题

问题描述

有如下基类:

public class ReportBase {
    protected List<ReportDataTable> ReportDataTables { get;set; }
}

多个报告类继承自该基类,例如:

public class MyCustomReport : ReportBase {
    public MyCustomReport() {}
    public void GenerateReport(string filters) {
      ReportDataTables = new List<ReportDataTable>();
      // 业务逻辑实现
    }
}

使用Autofac动态切换报告服务:

public class ReportService : IReportService
{
    private readonly IEnumerable<Lazy<IReportGeneratorService, IReportGeneratorMetadata>> _reportGeneratorServices;

    public ReportService(IEnumerable<Lazy<IReportGeneratorService, IReportGeneratorMetadata>> reportGeneratorServices)
    {
        _reportGeneratorServices = reportGeneratorServices;
    }

    public TestMethod(){
      // 在Parallel.ForEach中调用时非线程安全
      var reportServiceLazyLoad = _reportGeneratorServices.FirstOrDefault(s => s.Metadata.Name == myMetaDataName);
    }
 }

核心问题

在Parallel.ForEach并发调用同一报告类型、传入不同过滤器时,底层ReportDataTables会混入多份数据。验证发现,每次从集合中获取的是同一实例:

public class Test {

    private readonly IEnumerable<Lazy<IReportGeneratorService, IReportGeneratorMetadata>> _reportGeneratorServices;

    public Test(IEnumerable<Lazy<IReportGeneratorService, IReportGeneratorMetadata>> reportGeneratorServices)
        {
            _reportGeneratorServices = reportGeneratorServices;
        }

     public void CheckRefEquality(){
           var t1 = _reportGeneratorServices.FirstOrDefault(s => s.Metadata.Name == "MyReportService");
            var t2 = _reportGeneratorServices.FirstOrDefault(s => s.Metadata.Name == "MyReportService");
            var t3 = Object.ReferenceEquals(t1.Value, t2.Value);
           // t3返回true,说明始终获取同一实例,这是数据混乱的根源
   }
}

需要在并发场景下,每次获取全新的报告服务实例,避免共享实例导致的数据冲突:

Parallel.ForEach(filters, f => {
    var report =  _reportsCollection.FirstOrDefault(s => s.Metadata.Name == "AlwaysTheSameReport");
    // 期望每次都是新实例,但实际始终是同一个
    report.GetErDone(f);
});
解决方案

1. 修改Autofac注册的生命周期

默认情况下,如果报告服务注册时使用了SingleInstance()或未指定正确的生命周期,会导致每次获取同一实例。需要将报告生成器注册为每次解析都创建新实例,即使用InstancePerDependency():

// Autofac容器注册代码
builder.RegisterType<MyCustomReport>()
       .As<IReportGeneratorService>()
       .WithMetadata<IReportGeneratorMetadata>(m => m.For(meta => meta.Name, "MyReportService"))
       .InstancePerDependency(); // 关键:每次解析创建新实例

InstancePerDependency是Autofac的默认生命周期,但如果之前显式指定了SingleInstance(),必须替换为该配置。

2. 调整实例获取方式:用工厂/索引替代Lazy解析

Lazy<T>本身的特性是首次访问Value时创建实例,之后复用。因此需要改用工厂模式或Autofac的索引功能,确保每次请求都生成新实例。

方式一:使用IIndex<TKey, TService>索引(推荐)

Autofac的IIndex<TKey, TService>支持直接通过键值获取服务,且每次解析都会创建新实例(只要生命周期配置正确):

  1. 注册时用报告名称作为键:
builder.RegisterType<MyCustomReport>()
       .As<IReportGeneratorService>()
       .Keyed<IReportGeneratorService>("MyReportService") // 用名称作为索引键
       .InstancePerDependency();
  1. 在服务中注入索引并使用:
public class ReportService : IReportService
{
    private readonly IIndex<string, IReportGeneratorService> _reportGeneratorIndex;

    public ReportService(IIndex<string, IReportGeneratorService> reportGeneratorIndex)
    {
        _reportGeneratorIndex = reportGeneratorIndex;
    }

    public void TestMethod(string myMetaDataName){
      // 每次调用都会创建新实例
      var reportService = _reportGeneratorIndex[myMetaDataName];
    }
 }

方式二:自定义工厂委托

如果需要更灵活的逻辑,可以注册一个根据名称创建实例的工厂:

  1. 注册工厂:
builder.Register<Func<string, IReportGeneratorService>>(c =>
{
    var context = c.Resolve<IComponentContext>();
    return name =>
    {
        var registration = context.ComponentRegistry.Registrations
            .Where(r => r.HasMetadata<IReportGeneratorMetadata>() && r.Metadata.Get<IReportGeneratorMetadata>().Name == name)
            .FirstOrDefault();
        return registration != null ? context.Resolve(registration.Activator.LimitType) as IReportGeneratorService : null;
    };
});
  1. 注入工厂并使用:
public class ReportService : IReportService
{
    private readonly Func<string, IReportGeneratorService> _reportGeneratorFactory;

    public ReportService(Func<string, IReportGeneratorService> reportGeneratorFactory)
    {
        _reportGeneratorFactory = reportGeneratorFactory;
    }

    public void TestMethod(string myMetaDataName){
      // 每次调用都会创建新实例
      var reportService = _reportGeneratorFactory(myMetaDataName);
    }
 }

3. 并发场景验证

修改后,在Parallel.ForEach中调用时,每个线程都会获取独立的实例,ReportDataTables不会再共享数据:

Parallel.ForEach(filters, f => {
    var report = _reportGeneratorIndex["AlwaysTheSameReport"];
    report.GenerateReport(f); // 每个线程拥有独立实例,数据隔离
});

额外建议

  • 尽量将所有状态封装在实例内部,避免使用静态变量或全局共享资源
  • 若必须使用共享资源,改用线程安全集合(如ConcurrentBag<T>)或添加锁机制

内容的提问来源于stack exchange,提问作者99823

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 21:39:18