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>支持直接通过键值获取服务,且每次解析都会创建新实例(只要生命周期配置正确):
- 注册时用报告名称作为键:
builder.RegisterType<MyCustomReport>() .As<IReportGeneratorService>() .Keyed<IReportGeneratorService>("MyReportService") // 用名称作为索引键 .InstancePerDependency();
- 在服务中注入索引并使用:
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]; } }
方式二:自定义工厂委托
如果需要更灵活的逻辑,可以注册一个根据名称创建实例的工厂:
- 注册工厂:
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; }; });
- 注入工厂并使用:
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
相关产品推荐
相关产品推荐

