阻止 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;
}
我注意到的是:
如果至少一个 IPipeline 对象包含实际的异步方法(await Task.Delay(1)),则所有内容运行很顺利,我可以使用 CTRL+C 优雅地停止服务。
如果allIPipeline对象包含awaitTask.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 将是),则 while
和 ExecuteAsync
方法从不 "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。
我在 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;
}
我注意到的是:
如果至少一个 IPipeline 对象包含实际的异步方法(await Task.Delay(1)),则所有内容运行很顺利,我可以使用 CTRL+C 优雅地停止服务。
如果allIPipeline对象包含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 将是),则 while
和 ExecuteAsync
方法从不 "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。