.NET Core 3.1 LTSのGeneric Host(汎用ホスト)にはStart/Run/RunConsoleAsyncなど複数の起動方法がある。
static async Task Main(string[] args) { // パターン 1. - RunConsoleAsync await CreateHostBuilder(args).RunConsoleAsync(); // パターン 2. - Start/StartAsync await CreateHostBuilder(args).StartAsync(); // パターン 3. BuildしてRun/RunAsync await CreateHostBuilder(args).Build().RunAsync(); // パターン 4. BuildしてStart/StartAsync await CreateHostBuilder(args).Build().StartAsync(); }
それぞれの違いがいまいち分からないので、実装を眺めてみた。
目次
- 目次
- パターン 1. - RunConsoleAsync
- パターン 2. - Start/StartAsync
- パターン 3. - BuildしてRun/RunAsync
- パターン 4. - BuildしてStart/StartAsync
- つまりどういう差があるの?
- 実装
- StartAsync()の注意点
- おわり
パターン 1. - RunConsoleAsync
IHostBuilder
のRunConsoleAsync()はUseConsoleLifetime().Build().StartAsync()
を実行してからWaitForShutdownAsync()でCtrl+CまたはIHostApplicationLifetime
のStopApplication()によるShutdownを待機する。
パターン 2. - Start/StartAsync
IHostBuilder
のStartAsync()は内部でBuild().StartAsync()
を実行する。
パターン 3. - BuildしてRun/RunAsync
IHost
のRunAsync()はStartAsync()を実行してからWaitForShutdownAsync()でCtrl+CまたはIHostApplicationLifetime
のStopApplication()によるShutdownを待機する。
パターン 4. - BuildしてStart/StartAsync
IHost
のStartAsync()を実行する。
つまりどういう差があるの?
全パターンともIHostBuilder
をBuild()してIHost
インスタンスを生成した後StartAsync()する流れは共通であり、WaitForShutdownAsync()を実行するか否かが異なる。
※パターン 1, 3がWaitForShutdownAsync()で待機し、パターン 2,4は待機しない。
WaitForShutdownAsync()はCancellationTokenのキャンセル要求のCallbackを登録するのと、IHostApplication
のStopApplication()のイベントを待ち合わせする。
上記IHostApplication
のApplicationStopping
は意図的にStopApplication()を実行するか、Ctrl + CまたばSIGTERMをトリガに実行される。
※WaitForStartAsync()のConsole.CancelKeyPress
にStopApplication()を実行するイベントハンドラを登録することで実現している。
実装
肝になるStartAsync()
, WaitForShutdownAsync()
, UseConsoleLifetime()
を眺めてみる。
StartAsync()の実装
StartAsync()
はMicrosoft.Extensions.Hosting/Internal/Host.csで定義されている。
DI Containerに登録したIHostedService
の実装クラスをContainerから取り出し順次StartAsync()
で開始しているようだ。
上記処理の後、IHostApplicationLifetime
のApplicationStarted
を開始する。
public async Task StartAsync(CancellationToken cancellationToken = default) { _logger.Starting(); await _hostLifetime.WaitForStartAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); _hostedServices = Services.GetService<IEnumerable<IHostedService>>(); foreach (var hostedService in _hostedServices) { // Fire IHostedService.Start await hostedService.StartAsync(cancellationToken).ConfigureAwait(false); } // Fire IApplicationLifetime.Started _applicationLifetime?.NotifyStarted(); _logger.Started(); }
WaitForShutdownAsync()の実装
WaitForShutdownAsync()
はMicrosoft.Extensions.Hosting.Abstractions/HostingAbstractionsHostExtensions.csで定義されている。
CancellationTokenにキャンセル時の実行されるイベントを登録しキャンセルを待機する。
/// <summary> /// Returns a Task that completes when shutdown is triggered via the given token. /// </summary> /// <param name="host">The running <see cref="IHost"/>.</param> /// <param name="token">The token to trigger shutdown.</param> public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default) { var applicationLifetime = host.Services.GetService<IApplicationLifetime>(); token.Register(state => { ((IApplicationLifetime)state).StopApplication(); }, applicationLifetime); var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); applicationLifetime.ApplicationStopping.Register(obj => { var tcs = (TaskCompletionSource<object>)obj; tcs.TrySetResult(null); }, waitForStop); await waitForStop.Task; // Host will use its default ShutdownTimeout if none is specified. await host.StopAsync(); }
RunConsoleAsync()のUseConsoleLifetime()
UseConsoleLifetime()
はMicrosoft.Extensions.Hosting/Internal/ConsoleLifetime.csで定義されている。
/// <summary> /// Listens for Ctrl+C or SIGTERM and calls <see cref="IApplicationLifetime.StopApplication"/> to start the shutdown process. /// This will unblock extensions like RunAsync and WaitForShutdownAsync. /// </summary> /// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param> /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns> public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder) { return hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, ConsoleLifetime>()); }
ConsoleLifetime
は同ファイルに定義されている。中身は以下の通り。
ProcessExit
実行時にApplicationLifetime.StopApplication()
を実行を待つのとConsole.CancelKeyPress
のイベントを登録している。
public Task WaitForStartAsync(CancellationToken cancellationToken) { if (!Options.SuppressStatusMessages) { ApplicationLifetime.ApplicationStarted.Register(() => { Console.WriteLine("Application started. Press Ctrl+C to shut down."); Console.WriteLine($"Hosting environment: {Environment.EnvironmentName}"); Console.WriteLine($"Content root path: {Environment.ContentRootPath}"); }); } AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => { ApplicationLifetime.StopApplication(); _shutdownBlock.WaitOne(); }; Console.CancelKeyPress += (sender, e) => { e.Cancel = true; ApplicationLifetime.StopApplication(); }; // Console applications start immediately. return Task.CompletedTask; }
StartAsync()の注意点
public static async Task Main(string[] args) { await CreateHostBuilder(args).StartAsync(); }
上記コードのように、using
でwrapしない場合IHostedService
の処理を中断してもアプリケーションを終了することができなくなる。
※以下のメッセージが表示され、プロセスがゾンビ化する。
プロセスKillする羽目に…。
info: Microsoft.Hosting.Lifetime[0] Waiting for the host to be disposed. Ensure all 'IHost' instances are wrapped in 'using' blocks.
きちんとusing
でwrapすればCtrl + C, SIGTERM発生時にCancellationToken
による例外が飛ぶ。
info: Microsoft.Hosting.Lifetime[0] Application is shutting down... Unhandled exception. System.OperationCanceledException: The operation was canceled. at System.Threading.CancellationToken.ThrowOperationCanceledException() at System.Threading.CancellationToken.ThrowIfCancellationRequested() at Sample.Start.Worker.StartAsync(CancellationToken cancellationToken) in C:\Users\piyoe\work\Sample\Worker.cs:line 28 at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken) at Microsoft.Extensions.Hosting.HostingAbstractionsHostBuilderExtensions.StartAsync(IHostBuilder hostBuilder, CancellationToken cancellationToken) at Microsoft.Extensions.Hosting.HostingAbstractionsHostBuilderExtensions.Start(IHostBuilder hostBuilder) at Sample.Start.Program.Main(String[] args) in C:\Users\piyoe\work\dotnet\Sample\Program.cs:line 15 at Sample.Start.Program.<Main>(String[] args)
が、このままではIHostApplicationLifetime
のApplicationStarted
, ApplicationStopping
, ApplicationStopped
が正しい順に実行されない。
※OnStoppingがOnStartedの前に実行される。OnStoppedは実行されない。
info: Sample.Start.Worker[0] Worker - StartAsync info: Sample.Start.Worker[0] OnStopping info: Microsoft.Hosting.Lifetime[0] Application is shutting down... info: Sample.Start.Worker[0] OnStarted info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.
結局のところ、以下のコードのようにWaitForShutdown
またはWaitForShutdownAsync
と併用することが望ましい。
public static async Task Main(string[] args) { using var host = await CreateHostBuilder(args).StartAsync(); await host.WaitForShutdownAsync(); }
結局WaitForShutdownAsync()
で待つことになるのであれば、特別な理由がない限りRunConsoleAsync()
かBUild().RunAsync()
を使った方がよさそうだ。
もしくはこんな感じとか...。
using var host = CreateHostBuilder(args).Build();
await host.StartAsync();
await host.StopAsync();
おわり
RunConsoleAsync
で開始してIHostApplicationLifetime
でStart, Stopping, Stoppedをハンドリングするのが安定って感じがする。
IHost側はこんな感じでしたが、IWebHost側はどんな感じなんでしょうね。