如何解决IHost.RunAsync永不返回
我正在构建一个.NET Core 3.1应用程序,它将在Docker容器中运行BackgroundService
。尽管我已经为BackgroundService实现了启动和关闭任务,并且通过SIGTERM
触发该服务时,该服务肯定已关闭,但我发现await host.RunAsync()
调用从未完成,这意味着我的其余代码Main()
块未执行。
我是否错过了某些东西?或者我不应该期望RunAsync()
调用在后台服务完成停止后返回控制权吗?
(已更新,我可以提出的最简单的repro ...)
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace BackgroundServiceTest
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Main: starting");
try
{
using var host = CreateHostBuilder(args).Build();
Console.WriteLine("Main: Waiting for RunAsync to complete");
await host.RunAsync();
Console.WriteLine("Main: RunAsync has completed");
}
finally
{
Console.WriteLine("Main: stopping");
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseConsoleLifetime()
.ConfigureServices((hostContext,services) =>
{
services.AddHostedService<Worker>();
// give the service 120 seconds to shut down gracefully before whacking it forcefully
services.Configure<HostOptions>(options => options.ShutdownTimeout = TimeSpan.FromSeconds(120));
});
}
class Worker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("Worker: ExecuteAsync called...");
try
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(10),stoppingToken);
Console.WriteLine("Worker: ExecuteAsync is still running...");
}
}
catch (OperationCanceledException) // will get thrown if TaskDelay() gets cancelled by stoppingToken
{
Console.WriteLine("Worker: OperationCanceledException caught...");
}
finally
{
Console.WriteLine("Worker: ExecuteAsync is terminating...");
}
}
public override Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Worker: StartAsync called...");
return base.StartAsync(cancellationToken);
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Worker: StopAsync called...");
await base.StopAsync(cancellationToken);
}
public override void dispose()
{
Console.WriteLine("Worker: dispose called...");
base.dispose();
}
}
}
Dockerfile:
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim AS base
workdir /app
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
workdir /src
copY ["BackgroundServiceTest.csproj","./"]
RUN dotnet restore "BackgroundServiceTest.csproj"
copY . .
workdir "/src/"
RUN dotnet build "BackgroundServiceTest.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "BackgroundServiceTest.csproj" -c Release -o /app/publish
FROM base AS final
workdir /app
copY --from=publish /app/publish .
ENTRYPOINT ["dotnet","BackgroundServiceTest.dll"]
docker-compose.yml:
version: '3.4'
services:
backgroundservicetest:
image: ${DOCKER_REGISTRY-}backgroundservicetest
build:
context: .
dockerfile: Dockerfile
通过docker-compose up --build
运行此命令,然后在第二个窗口中运行docker stop -t 90 backgroundservicetest_backgroundservicetest_1
控制台输出显示Worker关闭并被处置,但是应用程序(显然)在RunAsync()
返回之前终止。
Successfully built 3aa605d4798f
Successfully tagged backgroundservicetest:latest
Recreating backgroundservicetest_backgroundservicetest_1 ... done
Attaching to backgroundservicetest_backgroundservicetest_1
backgroundservicetest_1 | Main: starting
backgroundservicetest_1 | Main: Waiting for RunAsync to complete
backgroundservicetest_1 | Worker: StartAsync called...
backgroundservicetest_1 | Worker: ExecuteAsync called...
backgroundservicetest_1 | info: Microsoft.Hosting.Lifetime[0]
backgroundservicetest_1 | Application started. Press Ctrl+C to shut down.
backgroundservicetest_1 | info: Microsoft.Hosting.Lifetime[0]
backgroundservicetest_1 | Hosting environment: Production
backgroundservicetest_1 | info: Microsoft.Hosting.Lifetime[0]
backgroundservicetest_1 | Content root path: /app
backgroundservicetest_1 | Worker: ExecuteAsync is still running...
backgroundservicetest_1 | Worker: ExecuteAsync is still running...
backgroundservicetest_1 | info: Microsoft.Hosting.Lifetime[0]
backgroundservicetest_1 | Application is shutting down...
backgroundservicetest_1 | Worker: StopAsync called...
backgroundservicetest_1 | Worker: OperationCanceledException caught...
backgroundservicetest_1 | Worker: ExecuteAsync is terminating...
backgroundservicetest_1 | Worker: dispose called...
backgroundservicetest_backgroundservicetest_1 exited with code 0
解决方法
在 lengthy discussion on Github 之后,结果是一些小的重构解决了问题。简而言之,.RunAsync()
会阻塞,直到主机完成并处理主机实例,这(显然)终止了应用程序。
通过将代码更改为调用 .StartAsync()
和 await host.WaitForShutdownAsync()
,控制确实按预期返回到 Main()
。最后一步是将主机放置在 finally
块中,如下所示:
static async Task Main(string[] args)
{
Console.WriteLine("Main: starting");
IHost host = null;
try
{
host = CreateHostBuilder(args).Build();
Console.WriteLine("Main: Waiting for RunAsync to complete");
await host.StartAsync();
await host.WaitForShutdownAsync();
Console.WriteLine("Main: RunAsync has completed");
}
finally
{
Console.WriteLine("Main: stopping");
if (host is IAsyncDisposable d) await d.DisposeAsync();
}
}
,
您应该使用RunConsoleAsync而不是RunAsync。只有RunConsoleAsync
会监听Ctrl + C或SIGTERM:
RunConsoleAsync启用控制台支持,构建并启动主机,并等待Ctrl + C / SIGINT或SIGTERM关闭。
代码应更改为:
await CreateHostBuilder(args).RunConsoleAsync();
这等同于在构建主机之前在主机构建器上调用UseConsoleLifeTime:
var host=CreateHostBuilder(args).UseConsoleLifetime().Build();
...
await host.RunAsync();
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。