1. 程式人生 > >[ASP.NET Core 3框架揭祕]服務承載系統[3]:總體設計[上篇]

[ASP.NET Core 3框架揭祕]服務承載系統[3]:總體設計[上篇]

前面的例項演示了服務承載的基本程式設計模式,接下來我們從設計的角度來重新認識服務承載模型。總的來說,服務承載模型主要由如下圖所示的三個核心物件組成:多個通過IHostedService介面表示的服務被承載於通過IHost介面表示的宿主上,IHostBuilder介面表示IHost物件的構建者。

一、IHostedService

承載的服務總是會被定義成IHostedService介面的實現型別。如下面的程式碼片段所示,該介面僅定義了兩個用來啟動和關閉自身服務的方法。當作為宿主的IHost物件被啟動的時候,它會利用依賴注入框架啟用每個註冊的IHostedService服務,並通過呼叫StartAsync方法來啟動它們。當服務承載應用程式關閉的時候,作為服務宿主的IHost物件會被關閉,由它承載的每個IHostedService服務物件的StopAsync方法也隨之被呼叫。

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

承載系統無縫集成了.NET Core的依賴注入框架,在服務承載過程中所需的依賴服務,包括承載服務自身和它所依賴的服務均由此框架提供,承載服務註冊的本質就是將對應的IHostedService實現型別或者例項註冊到依賴注入框架中。由於承載服務大都需要長時間執行直到應用被關閉,所以針對承載服務的註冊一般採用Singleton生命週期模式。承載系統為承載服務的註冊定義瞭如下這個AddHostedService<THostedService>擴充套件方法。由於該方法通過呼叫TryAddEnumerable擴充套件方法來註冊服務,所以不用擔心服務重複註冊的問題。

public static class ServiceCollectionHostedServiceExtensions
{
    public static IServiceCollection AddHostedService<THostedService>(this IServiceCollection services) where THostedService: class, IHostedService
    {
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>());
        return services;
    }
}

二、IHost

通過IHostedService介面表示的承載服務最終被承載於通過IHost介面表示的宿主上。一般來說,一個服務承載應用在整個生命週期內只會建立一個IHost物件,我們啟動和關閉應用程式本質上就是啟動和關閉作為宿主的IHost物件。如下面的程式碼片段所示,IHost介面派生於IDisposable介面,所以當它在關閉之後,應用程式還會呼叫其Dispose方法作一些額外的資源釋放工作。IHost介面的Services屬性返回作為依賴注入容器的IServiceProvider物件,該物件提供了服務承載過程中所需的服務例項,其中就包括需要承載的IHostedService服務。定義在IHost介面中的StartAsync和StopAsync方法完成了針對服務宿主的啟動和關閉。

public interface IHost : IDisposable
{
    IServiceProvider Services { get; }
    Task StartAsync(CancellationToken cancellationToken = default);
    Task StopAsync(CancellationToken cancellationToken = default);
}

三、應用生命週期

在前面演示的例項中,在利用HostBuilder物件構建出IHost物件之後,我們並沒有呼叫其StartAsync方法啟動它,而是另一個名為Run的擴充套件方法。Run方法涉及到服務承載應用生命週期的管理,如果想了解該方法的本質,就得先來認識一個名為IHostApplicationLifetime的介面。顧名思義,IHostApplicationLifetime介面體現了服務承載應用程式的生命週期。如下面的程式碼片段所示,該介面除了提供了三個CancellationToken型別的屬性來檢測應用何時開啟與關閉之外,還提供了一個StopApplication來關閉應用程式。

public interface IHostApplicationLifetime
{
    CancellationToken ApplicationStarted { get; }
    CancellationToken ApplicationStopping { get; }
    CancellationToken ApplicationStopped { get; }

    void StopApplication();
}

如下所示的ApplicationLifetime型別是對IHostApplicationLifetime介面的預設實現。我們可以看到它實現的三個屬性返回的CancellationToken物件來源於三個對應的CancellationTokenSource物件,後者對應著三個不同的方法(NotifyStarted、StopApplication和NotifyStopped)。我們可以利用IHostApplicationLifetime服務的三個屬性提供的CancellationToken物件得到關於應用被啟動和關閉通知,這些通知最初就是由這三個對應的方法發出來的。

public class ApplicationLifetime : IHostApplicationLifetime
{
    private readonly ILogger<ApplicationLifetime> _logger;
    private readonly CancellationTokenSource _startedSource;
    private readonly CancellationTokenSource _stoppedSource;
    private readonly CancellationTokenSource _stoppingSource;

    public ApplicationLifetime(ILogger<ApplicationLifetime> logger)
    {
        _startedSource = new CancellationTokenSource();
        _stoppedSource = new CancellationTokenSource();
        _stoppingSource = new CancellationTokenSource();
        _logger = logger;
    }

    private void ExecuteHandlers(CancellationTokenSource cancel)
    {
        if (!cancel.IsCancellationRequested)
        {
            cancel.Cancel(false);
        }
    }

    public void NotifyStarted()
    {
        try
        {
            this.ExecuteHandlers(this._startedSource);
        }
        catch (Exception exception)
        {
            _logger.ApplicationError(6, "An error occurred starting the application",exception);
        }
    }

    public void NotifyStopped()
    {
        try
        {
            ExecuteHandlers(this._stoppedSource);
        }
        catch (Exception exception)
        {
            _logger.ApplicationError(8, "An error occurred stopping the application",exception);
        }
    }

    public void StopApplication()
    {
        CancellationTokenSource source = this._stoppingSource;
        lock (source)
        {
            try
            {
                ExecuteHandlers(this._stoppingSource);
            }
            catch (Exception exception)
            {
                _logger.ApplicationError(7, "An error occurred stopping the application", exception);
            }
        }
    }

    public CancellationToken ApplicationStarted => _startedSource.Token;
    public CancellationToken ApplicationStopped => _stoppedSource.Token;
    public CancellationToken ApplicationStopping => _stoppingSource.Token;
}

四、利用IHostApplicationLifetime關閉應用

我們接下來通過一個簡單的例項來演示如何利用IHostApplicationLifetime服務來關閉整個承載應用程式。我們在一個控制檯應用程式中定義瞭如下這個承載服務FakeHostedService。在FakeHostedService型別的建構函式中,我們注入了IHostApplicationLifetime服務。在得到其三個屬性返回的CancellationToken物件之後,我們在它們上面分別註冊了一個回撥,回撥操作通過在控制檯上輸出相應的文字使我們可以知道應用程式何時被啟動和關閉。

public sealed class FakeHostedService : IHostedService
{
    private readonly IHostApplicationLifetime _lifetime;
    private IDisposable _tokenSource;

    public FakeHostedService(IHostApplicationLifetime lifetime)
    {
        _lifetime = lifetime;
        _lifetime.ApplicationStarted.Register(() => Console.WriteLine("[{0}]Application started", DateTimeOffset.Now));
        _lifetime.ApplicationStopping.Register(() => Console.WriteLine("[{0}]Application is stopping.", DateTimeOffset.Now));
        _lifetime.ApplicationStopped.Register(() => Console.WriteLine("[{0}]Application stopped.", DateTimeOffset.Now));
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token.Register(_lifetime.StopApplication);
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _tokenSource?.Dispose();
        return Task.CompletedTask;
    }
}

在實現的StartAsync方法中,我們採用如上的方式在5秒之後呼叫IHostApplicationLifetime服務的StopApplication方法來關閉整個應用程式。這個FakeHostedService服務最後採用如下的方式承載於當前應用程式中。

class Program
{
    static void Main()
    {
        new HostBuilder()
            .ConfigureServices(svcs => svcs.AddHostedService<FakeHostedService>())
            .Build()
            .Run();
    }
}

該程式執行之後會在控制檯上輸出如下圖所示的結果,從三條訊息產生的時間間隔我們可以確定當前應用程式正是承載FakeHostedService通過呼叫IHostApplicationLifetime服務的StopApplication方法關閉的。(原始碼從這裡下載)

五、Run擴充套件方法

如果我們呼叫IHost物件的擴充套件方法Run,它會在內部呼叫StartAsync方法,接下來它會持續等待下去直到接收到應用被關閉的通知。當IHost物件物件利用IHostApplicationLifetime服務接收到關於應用關閉的通知後,它會呼叫自身的StopAsync方法,針對Run方法的呼叫此時才會返回。啟動IHost物件直到應用關閉這一實現體現在如下這個WaitForShutdownAsync擴充套件方法上。

public static class HostingAbstractionsHostExtensions
{
    public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default)
    {
        var applicationLifetime = host.Services.GetService<IHostApplicationLifetime>();
        token.Register(state => ((IHostApplicationLifetime)state).StopApplication(), applicationLifetime);

        var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
        applicationLifetime.ApplicationStopping.Register(state =>
        {
            var tcs = (TaskCompletionSource<object>)state;
            tcs.TrySetResult(null);
        }, waitForStop);

        await waitForStop.Task;
        await host.StopAsync();
    }
}

如下所示的WaitForShutdown方法是上面這個WaitForShutdownAsync方法的同步版本。同步的Run方法和非同步的RunAsync方法的實現也體現在下面的程式碼片段中。除此之外,下面的程式碼片段還提供了Start和StopAsync這兩個擴充套件方法,前者可以視為StartAsync方法的同步版本,後者可以在關閉IHost物件的時候指定一個超時時限。

public static class HostingAbstractionsHostExtensions
{
    public static void WaitForShutdown(this IHost host) => host.WaitForShutdownAsync().GetAwaiter().GetResult();
    public static void Run(this IHost host) => host.RunAsync().GetAwaiter().GetResult();
    public static async Task RunAsync(this IHost host, CancellationToken token = default)
    {
        try
        {
            await host.StartAsync(token);
            await host.WaitForShutdownAsync(token);
        }
        finally
        {
            host.Dispose();
        }
    }
    public static void Start(this IHost host) => host.StartAsync().GetAwaiter().GetResult();
    public static Task StopAsync(this IHost host, TimeSpan timeout) => host.StopAsync(new CancellationTokenSource(timeout).Token);
}

服務承載系統[1]: 承載長時間執行的服務[上篇]
服務承載系統[2]: 承載長時間執行的服務[下篇]
服務承載系統[3]: 總體設計[上篇]
服務承載系統[4]: 總體設計[下篇]
服務承載系統[5]: 承載服務啟動流程[上篇]
服務承載系統[6]: 承載服務啟動流程