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

ASP.NET 7中动态创建带作用域依赖的长期运行任务(无编译时托管服务)

需求背景

我正在使用ASP.NET,需要从任意作用域服务、单例服务或托管服务内部创建并启动长期运行任务。任务的数量和创建时机在编译时无法确定,而是在处理HTTP请求或BackgroundService循环过程中,当需要执行大量CPU密集型工作(比如模拟计算)时,才在线程池创建多个长期运行任务。此外,这些长期运行任务需要使用生命周期与其自身一致的作用域依赖项。

设计的任务启动器解决方案
public interface ILongRunningTaskStarter
{
    public void FireAndForget<TWorkDependencies>(Func<TWorkDependencies, CancellationToken, Task> longRunningWork) where TWorkDependencies : notnull;
}

public class LongRunningTaskStarter : ILongRunningTaskStarter
{
    protected readonly ILogger<LongRunningTaskStarter> _logger;
    private readonly IServiceProvider _serviceProvider;
    private readonly CancellationTokenSource _stoppingCts  = new CancellationTokenSource();

    public LongRunningTaskStarter(ILogger<LongRunningTaskStarter> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    private Func<CancellationToken, Task> WrapInScope<TWorkDependencies>(Func<TWorkDependencies, CancellationToken, Task> longRunningWork) where TWorkDependencies : notnull
    {
        return async (stoppingToken) =>
        {
            try
            {
                _logger.LogDebug("Long running task on Thread {ThreadId} has started.", Thread.CurrentThread.ManagedThreadId);
                using var scope = _serviceProvider.CreateScope();
                var scopedDependencies = scope.ServiceProvider.GetRequiredService<TWorkDependencies>();
                await longRunningWork(scopedDependencies, stoppingToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                _logger.LogDebug("Long running task on Thread {ThreadId} has stopped.", Thread.CurrentThread.ManagedThreadId);
                throw;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Uncaught exception from long running task on Thread {ThreadId}.", Thread.CurrentThread.ManagedThreadId);
            }
        };
    }

    public void FireAndForget<TWorkDependencies>(Func<TWorkDependencies, CancellationToken, Task> longRunningWork) where TWorkDependencies : notnull
    {
        var stoppingToken = _stoppingCts.Token;
        var scopedWork = WrapInScope(longRunningWork);
        Task.Run(() => scopedWork(stoppingToken), stoppingToken).ConfigureAwait(false);
    }
}
使用示例
public class SimulationExampleComponent : ISimulationExampleComponent
{
    private ILongRunningTaskStarter _taskStarter;

    public SimulationExampleComponent(ILongRunningTaskStarter taskStarter)
    {
        _taskStarter = taskStarter;
    }

    public async Task ExecuteAvailableSimulationJobs()
    {
        var simulationInputList = await RequestSimulationJobsFromServer();
        foreach (var simulationInput in simulationInputList)
        {
            var longRunningWork = (ISimulationDependencies dep, CancellationToken token) => Simulate(dep, token, simulationInput);
            _taskStarter.FireAndForget(longRunningWork);
        }
    }

    private static Task<double[]> RequestSimulationJobsFromServer()
    {
        var dummyInput = new Random().NextDoubleSequence().Take(100).ToArray();
        return Task.FromResult(dummyInput);
    }

    private static async Task Simulate(ISimulationDependencies dep, CancellationToken stoppingToken, double simulationInput)
    {
        // 实际的长期运行工作:
        //      1. 在发送状态事件的同时执行模拟所需操作。
        //      2. 将输出写入数据存储。
        //      3. 通知相关方模拟已完成。
    }
}
服务注册
services.AddScoped<ISimulationDependencies, SimulationDependencies>();
services.AddSingleton<ILongRunningTaskStarter, LongRunningTaskStarter>();
services.AddScoped<ISimulationExampleComponent, SimulationExampleComponent>();
技术疑问解答

1. 代码是否存在死锁风险,尤其是在scope.ServiceProvider.GetRequiredService<TWorkDependencies>()调用时?

当前代码不存在明显死锁风险。GetRequiredService是同步调用,但这里是在Task.Run启动的线程池线程中执行,没有阻塞ASP.NET请求上下文的同步上下文(因为用了ConfigureAwait(false)且在非请求线程中)。只要TWorkDependencies的构造函数中没有同步等待异步操作且阻塞当前线程的逻辑,就不会触发死锁。

2. 是否存在依赖项被提前释放的风险?await longRunningWork是否能确保作用域不会被提前释放?

是的,await longRunningWork能保证作用域不会提前释放。因为scope是用using var声明的,其释放会延迟到await完成之后的代码块末尾。只要longRunningWork内部没有提前释放依赖项的手动操作,作用域内的依赖项会在任务完成后才被释放,不存在提前释放的问题。

3. .ConfigureAwait(false)调用对访问作用域有影响吗?作用域是否与线程上下文正交且独立?

ConfigureAwait(false)对作用域访问没有影响。作用域是基于IServiceScope实例的,与线程上下文完全正交。只要持有有效的IServiceScope实例,无论在哪个线程上访问其ServiceProvider获取依赖项,都是安全的。ConfigureAwait(false)只是避免捕获原上下文,不会改变作用域的有效性。

4. ASP.NET DI框架是否会出现嵌套作用域的问题?比如在作用域A中创建任务,任务再创建作用域B?

嵌套作用域是ASP.NET DI支持的正常场景,不会有问题。作用域B是从根服务提供者创建的(因为LongRunningTaskStarter是单例,注入的IServiceProvider是根容器),并非嵌套在作用域A中。每个任务的作用域都是独立的根作用域的子作用域,彼此隔离,不会继承或干扰原请求的作用域A。

5. 是否有更好的实现方法?

你的方案本质上是服务定位器模式,虽然符合常见做法,但可以做一些优化:

  • 支持任务追踪:当前是Fire-and-Forget,无法追踪任务状态。可以修改FireAndForget返回Task,让调用方可选追踪任务(但要注意调用方不能阻塞等待)。
  • 集成主机停止逻辑:当前的_stoppingCts没有关联主机的停止信号,应该让LongRunningTaskStarter实现IHostedService,在StopAsync中调用_stoppingCts.Cancel(),确保主机停止时能正确终止所有任务。
  • 适配异步释放:如果TWorkDependencies实现了IAsyncDisposable,当前的using var scope会自动调用DisposeAsync,确保异步资源被正确释放。
  • 弱化服务定位器味:可以通过泛型工厂注册的方式,将依赖项的解析提前到DI容器层面,但对于动态创建的任务,服务定位器模式本身是合理的折中方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 01:48:28