阻止 AspNetCore 正确 start/stop 的 BackgroundService 实现

BackgroundService implementation that prevents AspNetCore to start/stop correctly

我在 aspnet 核心应用程序中使用 BackgroundService 对象。

关于ExecuteAsync方法中运行的操作实现方式,Aspnet核心无法正确初始化或停止。这是我尝试过的:

我按照 documentation 中解释的方式实现了抽象 ExecuteAsync 方法。

管道变量是在构造函数中注入的IEnumerable<IPipeline>

public interface IPipeline {
    Task Start();
    Task Cycle();
    Task Stop();
}

...

protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
    log.LogInformation($"Starting subsystems");

    foreach(var engine in pipeLine) {
        try {
            await engine.Start();
        }
        catch(Exception ex) {
            log.LogError(ex, $"{nameof(engine)} failed to start");
        }
    }

    log.LogInformation($"Runnning main loop");

    while(!stoppingToken.IsCancellationRequested) {
        foreach(var engine in pipeLine) {
            try {
                await engine.Cycle();
            }
            catch(Exception ex) {
                log.LogError(ex, $"{engine.GetType().Name} error in Cycle");
            }
        }
    }

    log.LogInformation($"Stopping subsystems");

    foreach(var engine in pipeLine) {
        try {
            await engine.Stop();
        }
        catch(Exception ex) {
            log.LogError(ex, $"{nameof(engine)} failed to stop");
        }
    }
}

由于项目的当前开发状态​​,有许多 "nop" 管道包含以这种方式实现的空 Cycle() 操作:

public async Task Cycle() {
    await Task.CompletedTask;
}

我注意到的是:

那么一方面是aspnetcore没能正确初始化。我的意思是,控制台上没有 "Now listening on: http://[::]:10001 Application started. Press Ctrl+C to shut down."。

另一方面,当我按下 CTRL+C 时,控制台显示 "Application is shutting down...",但循环继续 运行,就好像 CancellationToken 从未被请求停止一样。

所以基本上,如果我将单个 Pipeline 对象更改为此:

public async Task Cycle() {
    await Task.Delay(1);
}

然后一切正常,我不明白为什么。有人可以解释一下我对任务处理不了解的地方吗?

最简单的解决方法是将 await Task.Yield(); 添加为 ExecuteAsync.

的第一行

我不是专家...但是 "problem" 是这个 ExecuteAsync 中的所有代码实际上 运行 是同步的。

如果所有 "cycles" return 同步完成的任务(如 Task.CompletedTask 将是),则 whileExecuteAsync 方法从不 "yield"s.

该框架实质上对已注册的 IHostedService 执行 foreach 并调用 StartAsync。如果您的服务没有产生,则 foreach 将被阻止。因此任何其他服务(包括 AspNetCore 主机)都不会启动。由于引导无法完成,ctrl-C 处理等也永远无法设置。

await Task.Delay(1) 放入循环 "releases" 或 "yields" 任务之一。这允许主机 "capture" 任务并继续。这允许取消等连接发生。

Task.Yield() 放在 ExecuteAsync 的顶部只是实现此目的的更直接方法,意味着循环根本不需要知道这个 "issue"。

注意:这通常只是测试中的真正问题...'因为你为什么会有空操作周期?

注意 2:如果您可能有 "compute" 个周期(即它们不执行任何 IO、数据库、队列等),那么切换到 ValueTask 将有助于 perf/allocations。