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

.NET 6异步任务循环中如何为每个任务注入独立服务实例

问题背景

现有实现ISomethingManager接口的SomethingManager类,定义如下:

public class SomethingManager : ISomethingManager

该类位于.NET 6 Worker Service解决方案下的独立类库项目中,最初在Worker Service项目中按如下方式注册为单例:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSerilog()
        .ConfigureServices((hostContext, services) =>
        {
            //AddSingleton or Transient here?
            services.AddSingleton<ISomethingManager, SomethingManager>();
            // ...其他服务注册
        });

项目中存在并发循环处理逻辑:

foreach (xml in xmls)
{
    tasks.Add(StartProcessAsync(xml));
}
await Task.WhenAll(tasks);

StartProcessAsync方法内部通过构造函数注入使用SomethingManager实例,但SomethingManager包含多个需要在每个任务中独立持有的私有成员:每个任务执行前需要先调用.Connect()获取独立的sessionId,不同任务的sessionId不能共享,当前单例注册的方式会导致多任务并发时出现状态冲突错误。
核心疑问是:如何通过DI注册机制保证每个任务持有的SomethingManager实例私有成员相互独立?如果无法通过DI实现,是否需要每次任务执行时手动创建新实例?示例代码如下:

public Task StartProcessAsync(xmlFileInfo xml)
{
    return Task.Run(async () =>
    {
        // 直接使用构造函数注入的实例在并发任务中会出问题,因为所有任务共享同一个sessionId
        //_somethingManager.DoSomething();
        // 手动new实例是否可行?
        var somethingManager= SomethingManager(_someSettings);
        somethingManager.DoSomething();
        var mem = somethingManager.ThePrivateMember; 
        // 同类型的其他带私有成员的对象也存在相同问题
     });
}

解决方案

首先明确:当前用AddSingleton注册必然会出现状态冲突,因为单例服务在整个应用生命周期内仅创建一次,所有并发任务拿到的都是同一个实例,私有成员、sessionId会被共享。
直接把注册改成AddTransient也无法解决问题:Worker服务本身是单例生命周期(AddHostedService默认注册为单例),构造函数注入的服务只会在Worker实例化时创建一次,会被永久存到Worker的私有字段中,哪怕注册为Transient,实际使用的还是同一个实例,属于典型的** captive依赖问题**(短生命周期服务被长生命周期服务持有,实际生命周期被拉长到和宿主一致)。
下面给出两种符合DI规范的可行方案,不推荐直接手动new实例。

方案1:基于IServiceScopeFactory创建任务级作用域(推荐)

这是.NET DI原生支持的标准实现,自动管理服务生命周期和依赖注入,无需手动维护实例依赖。

  1. 调整服务注册,将SomethingManager注册为Scoped生命周期即可,其依赖的配置、其他服务按原有方式注册:
    .ConfigureServices((hostContext, services) =>
    {
        services.AddSerilog();
        services.AddScoped<ISomethingManager, SomethingManager>();
        // 注册配置绑定,例如SomethingManager依赖的_someSettings
        services.Configure<SomeSettings>(hostContext.Configuration.GetSection("SomeSettings"));
        services.AddHostedService<Worker>();
    });
    
  2. 不要在Worker构造函数中直接注入ISomethingManager,改为注入IServiceScopeFactory,在每个任务执行时创建独立的DI作用域,从作用域中获取独立的服务实例:
    public class Worker : BackgroundService
    {
        private readonly IServiceScopeFactory _scopeFactory;
    
        // 注入作用域工厂
        public Worker(IServiceScopeFactory scopeFactory)
        {
            _scopeFactory = scopeFactory;
        }
    
        public async Task StartProcessAsync(XmlFileInfo xml)
        {
            // 为当前任务创建独立的DI作用域,作用域内获取的Scoped/Transient服务都是独立实例
            using var scope = _scopeFactory.CreateScope();
            var somethingManager = scope.ServiceProvider.GetRequiredService<ISomethingManager>();
    
            // 实例完全独立,当前Connect生成的sessionId不会和其他任务冲突
            await somethingManager.Connect();
            somethingManager.DoSomething();
            var mem = somethingManager.ThePrivateMember;
    
            // 作用域using结束后,所有实现了IDisposable的服务会被容器自动释放,无需手动处理
        }
    
        // 其他原有逻辑...
    }
    

方案2:注册强类型工厂委托

如果不想每次创建作用域,可以注册工厂委托,每次调用工厂直接获取新实例。

  1. 调整服务注册:
    .ConfigureServices((hostContext, services) =>
    {
        services.AddSerilog();
        services.AddTransient<ISomethingManager, SomethingManager>();
        // 注册工厂委托,单例存储工厂
        services.AddSingleton<Func<ISomethingManager>>(sp => 
            () => sp.GetRequiredService<ISomethingManager>()
        );
        services.Configure<SomeSettings>(hostContext.Configuration.GetSection("SomeSettings"));
        services.AddHostedService<Worker>();
    });
    
  2. 构造函数注入工厂委托,每个任务执行时调用工厂获取新实例:
    public class Worker : BackgroundService
    {
        private readonly Func<ISomethingManager> _somethingManagerFactory;
    
        public Worker(Func<ISomethingManager> somethingManagerFactory)
        {
            _somethingManagerFactory = somethingManagerFactory;
        }
    
        public async Task StartProcessAsync(XmlFileInfo xml)
        {
            // 每次调用工厂都会返回全新的SomethingManager实例,实例间完全隔离
            using var somethingManager = _somethingManagerFactory();
            await somethingManager.Connect();
            somethingManager.DoSomething();
            var mem = somethingManager.ThePrivateMember;
        }
    }
    

注意:该方案下Transient实例如果被外部Dispose,容器不会重复释放;如果不手动加using包裹,实现了IDisposable的服务会被容器跟踪直到应用退出,存在内存泄漏风险,优先使用方案1。

不推荐手动new实例的原因

示例中直接new SomethingManager(_someSettings)的方式虽然能临时跑通,但存在明显缺陷:

  • 后续SomethingManager新增依赖(比如ILogger、其他业务服务)时,所有手动new的地方都要同步修改传参,维护成本极高
  • 无法享受DI容器的自动生命周期管理、依赖自动注入能力
  • 单元测试时无法方便地注入Mock替代实现,测试难度提升
    仅当SomethingManager是无任何外部依赖的纯静态工具类时,才考虑直接实例化。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 10:27:18