阻止 AspNetCore 正确启动/停止的 BackgroundService 实现

Lar*_*rry 2 c# task background-service asp.net-core

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

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

我按照文档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");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

由于项目当前的开发状态,有许多“nop”Pipeline 包含这样实现的空 Cycle() 操作:

public async Task Cycle() {
    await Task.CompletedTask;
}
Run Code Online (Sandbox Code Playgroud)

我注意到的是:

  • 如果至少一个IPipeline 对象包含实际的异步方法(await Task.Delay(1)),那么一切都会顺利运行,我可以使用 CTRL+C 优雅地停止服务。

  • 如果所有IPipeline 对象都包含 wait Task.CompletedTask;

那么一方面,aspnetcore无法正确初始化。我的意思是,没有“正在侦听:http://[::]:10001应用程序已启动。按 Ctrl+C 关闭。” 在控制台上。

另一方面,当我按下 CTRL+C 时,控制台显示“应用程序正在关闭...”,但循环继续运行,就好像 CancellationToken 从未被请求停止一样。

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

public async Task Cycle() {
    await Task.Delay(1);
}
Run Code Online (Sandbox Code Playgroud)

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

And*_*ook 6

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

我不是专家......但“问题”是其中的所有代码ExecuteAsync实际上都是同步运行的。

如果所有“周期”返回一个已同步完成的任务(如 Task.CompletedTask 所示),则该while方法ExecuteAsync永远不会“yield”。

IHostedService该框架本质上是对注册的s 和调用执行 foreach StartAsync。如果你的服务没有屈服,那么 foreach 就会被阻塞。因此任何其他服务(包括 AspNetCore 主机)都不会启动。由于引导无法完成,因此 ctrl-C 处理等也永远无法设置。

放入await Task.Delay(1)其中一个周期“释放”或“产生”任务。这允许主机“捕获”任务并继续。这允许取消等的连接发生。

放在Task.Yield()顶部ExecuteAsync只是实现此目的的更直接方法,并且意味着周期根本不需要意识到这个“问题”。

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

注意2:如果您可能有“计算”周期(即它们不执行任何IO、数据库、队列等),那么切换到ValueTask 将有助于性能/分配。