🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

.NET CoreのGeneric Host(汎用ホスト)のStart/Run/RunConsoleAsyncの違い

.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

IHostBuilderRunConsoleAsync()UseConsoleLifetime().Build().StartAsync()を実行してからWaitForShutdownAsync()でCtrl+CまたはIHostApplicationLifetimeStopApplication()によるShutdownを待機する。

パターン 2. - Start/StartAsync

IHostBuilderStartAsync()は内部でBuild().StartAsync()を実行する。

パターン 3. - BuildしてRun/RunAsync

IHostRunAsync()StartAsync()を実行してからWaitForShutdownAsync()でCtrl+CまたはIHostApplicationLifetimeStopApplication()によるShutdownを待機する。

パターン 4. - BuildしてStart/StartAsync

IHostStartAsync()を実行する。

つまりどういう差があるの?

全パターンともIHostBuilderBuild()してIHostインスタンスを生成した後StartAsync()する流れは共通であり、WaitForShutdownAsync()を実行するか否かが異なる。
※パターン 1, 3がWaitForShutdownAsync()で待機し、パターン 2,4は待機しない。

WaitForShutdownAsync()はCancellationTokenのキャンセル要求のCallbackを登録するのと、IHostApplicationStopApplication()のイベントを待ち合わせする。

上記IHostApplicationApplicationStoppingは意図的にStopApplication()を実行するか、Ctrl + CまたばSIGTERMをトリガに実行される。
WaitForStartAsync()Console.CancelKeyPressStopApplication()を実行するイベントハンドラを登録することで実現している。

実装

肝になるStartAsync(), WaitForShutdownAsync(), UseConsoleLifetime()を眺めてみる。

StartAsync()の実装

StartAsync()Microsoft.Extensions.Hosting/Internal/Host.csで定義されている。

DI Containerに登録したIHostedServiceの実装クラスをContainerから取り出し順次StartAsync()で開始しているようだ。

上記処理の後、IHostApplicationLifetimeApplicationStartedを開始する。

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)

が、このままではIHostApplicationLifetimeApplicationStarted, 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側はどんな感じなんでしょうね。