dotnet core 非常好用,代碼也及其精煉,但是,你真的搞懂了每一行代碼背后的含義了嗎?
本文希望能夠深入淺出地梳理一下它的脈絡,把它從神秘變成水晶一般透明。
本文關注於分析 Pragram.cs 代碼文件,深入分析其中的 Host 宿主處理機制。
新創建 Web 應用程序
使用下面的命令可以快速創建一個 dotnet core 的 Web 應用。
dotenet new web -n HelloWeb
生成的 Program.cs 中源代碼如下所示:
namespace HelloWeb
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Main 方法調用了自定義的 CreteaHostBuilder() 方法來得到一個 IHostBuilder 對象實例,並在調用其 Build() 方法來得到實際的 Host 對象,然后調用 Host 對象實例的 Run() 方法開始應用的執行過程。
在這個 CreateHostBuilder() 方法中,通過 Host 類型的靜態方法 CreateDefaultBuilder() 來得到 IHostBuilder 對象實例,並調用了此對象實例的 ConfigureWebHostDefaults() 方法。此方法返回的仍然是 IHostBuilder 實例本身。
通過類型名稱可以看出,這里使用了構建模式,這個 IHostBuilder 對象是用來構建一個 IHost 對象實例的,Build() 方法即構建出一個 IHost 對象實例,然后的 Run() 方法是調用了 IHost 對象實例的方法。
作為學習 Host 的第一篇文章,我們先關注 Host 本身,以后我們再看 IHostBuilder 是怎樣構建這個 Host 對象實例的。
Host
我們就從 Host 開始。
Host 本身作為整個應用程序的基石,主要作用是用來管理寄宿在自身之上的 IHostedService,負責把注冊的這些服務啟動起來,並提供應用程序的生命周期管理。
具體注冊了哪些服務,其實是通過 IHostBuilder 來完成的,所以,對於 Host 本身來說,並沒有提供多少擴展點,我們主要是理解它提供的功能為主。
在 ASP.NET Core 中,Host 是整個應用的基石,Web 應用也是作為一個服務是寄宿在這個 Host 之上的。所以,我們先跳過 IHostBuilder,首先從 IHost 開始。
Host 使用接口 IHost 定義,此接口的定義很簡潔,可以在 GitHub 中查看 IHost 源碼。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// A program abstraction.
/// </summary>
public interface IHost : IDisposable
{
IServiceProvider Services { get; }
Task StartAsync(CancellationToken cancellationToken = default);
Task StopAsync(CancellationToken cancellationToken = default);
}
}
其中 類型為 IServiceProvider 的 Services 用來提供依賴注入的支持,我們有專門介紹依賴注入的文章,請參考這里,這里我們需要知道的就是,可以通過它來獲取對象實例。
兩個方法分別用來啟動服務和停止服務。服務這個詞在 .NET Core 中在不同的場景下,有不同的含義,這里指的服務是 IHostedService 服務,也就是寄宿在 Host 中的服務,啟動服務就是調用 IHostedService 對象的啟動方法,而停止服務也就是調用這些服務的停止方法。
這些服務很可能運行在不同的線程之上,我們怎么通知一個線程優雅地結束掉,而不是粗暴地直接取消呢?在多線程模式下,我們一般會傳遞一個 CancellationToken 對象進去,通過它實現線程之間的通知。
不要被名字中的 Cancelation 所迷惑,它並不是僅僅用來取消操作的,更多的時候,是用來在多線程場景下,在不同的線程之間進行事件通知的。我們也會專門介紹這個 CancellationToken,這里先不深入進行了。
除了核心的 StartAsync() 和 StopAsync() 方法,另外,在 Host 的擴展方法定義 HostingAbstractionsHostExtensions 中,又定義了一組輔助方法:
- void Run(this IHost host)
- async Task RunAsync(this IHost host, CancellationToken token = default)
- void Start(this IHost host)
- Task StopAsync(this IHost host, TimeSpan timeout)
- void WaitForShutdown(this IHost host)
- async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default)
這里面提供的的 Run() 調用內部調用了 RunAsync() 方法,而 RunAsync() 方法又調用了 StartAsync() 方法用來啟動 Host 主機。
public static class HostingAbstractionsHostExtensions
{
// ......
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
{
if (host is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
host.Dispose();
}
}
}
// ......
}
同樣來自擴展方法的 WaitForShutdownAsync() 方法,提供了對 Host 停機的監控。
IHsot 接口定義的默認實現 Host
IHost 接口的默認實現是 Host 類,可以在 GitHub 中查看源碼。注意,它的命名空間是 Microsoft.Extensions.Hosting.Internal,就是說,這一個內部類,並不能在外部實例化。我們在主程序中看到的 Host 並不是這個 Host 類,那是另外一個 Host,我們后面介紹。
namespace Microsoft.Extensions.Hosting.Internal
{
internal class Host : IHost, IAsyncDisposable
{
查看這個 Host 的構造函數,可以看到它依賴多個服務的支持,其中最為重要的是兩個生命周期管理服務。
- 主機的生命周期管理
- 應用的生命周期管理
在 Host 的成員中,有兩個重要的成員
- IHostLifetime
- IHostApplicationLifetime
可以看到,它們都是從構造函數中注入的,在 dotnet core 中,構造中的參數應當看作服務依賴,也就是它所依賴的服務,這些被依賴的服務會在容器構建對象時,由容器提供。
public Host(
IServiceProvider services,
IHostApplicationLifetime applicationLifetime,
ILogger<Host> logger,
IHostLifetime hostLifetime,
IOptions<HostOptions> options)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
_applicationLifetime = (applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime))) as ApplicationLifetime;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_hostLifetime = hostLifetime ?? throw new ArgumentNullException(nameof(hostLifetime));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
主機的生命周期管理
這里需要注意的是,應用實際上分為了兩個層級,應用的宿主 Host 和應用 Application。
宿主 Host 是底層,它的生命周期因為寄宿的環境不同而存在多種形式。例如,當寄宿在 Console 下面的時候,這是默認情況,通過執行程序來啟動 Host,通過 Ctrl + C 可以終止 Host 的運行,進而導致整個應用程序的終止。而當寄宿在 Windows 服務下的時候,就可以通過服務管理器來啟動和終止 Host 的執行。
其中,IHostLifetime 用來提供宿主 Host 在啟動和停止時的通知機制。在 GitHub 中查看 IHostLifetime 的定義。
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Hosting
{
public interface IHostLifetime
{
/// <summary>
/// Called at the start of <see cref="IHost.StartAsync(CancellationToken)"/> which will wait until it's complete before
/// continuing. This can be used to delay startup until signaled by an external event.
/// </summary>
Task WaitForStartAsync(CancellationToken cancellationToken);
/// <summary>
/// Called from <see cref="IHost.StopAsync(CancellationToken)"/> to indicate that the host is stopping and it's time to shut down.
/// </summary>
Task StopAsync(CancellationToken cancellationToken);
}
}
該接口有多個實現,如果我們查看對於 Console 的實現,在 GitHub 中查看 ConsoleLifetime 源代碼。
public class ConsoleLifetime : IHostLifetime, IDisposable
{
private readonly ManualResetEvent _shutdownBlock = new ManualResetEvent(false);
private CancellationTokenRegistration _applicationStartedRegistration;
private CancellationTokenRegistration _applicationStoppingRegistration;
public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions)
: this(options, environment, applicationLifetime, hostOptions, NullLoggerFactory.Instance) { }
public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions, ILoggerFactory loggerFactory)
{
Options = options?.Value ?? throw new ArgumentNullException(nameof(options));
Environment = environment ?? throw new ArgumentNullException(nameof(environment));
ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
HostOptions = hostOptions?.Value ?? throw new ArgumentNullException(nameof(hostOptions));
Logger = loggerFactory.CreateLogger("Microsoft.Hosting.Lifetime");
}
例如,ConsoleLifetime 的 WaitForStartAsync() 方法中,就定義了對 CancelKeyPress 的處理。
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
if (!Options.SuppressStatusMessages)
{
_applicationStartedRegistration = ApplicationLifetime.ApplicationStarted.Register(state =>
{
((ConsoleLifetime)state).OnApplicationStarted();
},
this);
_applicationStoppingRegistration = ApplicationLifetime.ApplicationStopping.Register(state =>
{
((ConsoleLifetime)state).OnApplicationStopping();
},
this);
}
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
Console.CancelKeyPress += OnCancelKeyPress;
// Console applications start immediately.
return Task.CompletedTask;
}
當應用使用 Windows Service 的時候,則會使用基於 Windows 服務的實現,在 GitHub 中查看 WindowsServiceLifetime 的源代碼。
應用的生命周期
可以看到 ConsoleLifetime 的構造函數也通過依賴注入接受 IHostApplicationLifetime 對象實例。
這個 IHostApplicationLifetime 是什么呢?它負責應用程序本身的生命周期管理。它是獨立於 Host 之外的。在 GitHub 中查看 IHostApplicationLifetime 源代碼。它的定義非常簡單。基於 CancellationToken,可以注冊多個回調方法,可以在應用啟動、停止前和停止后分別得到通知。
using System.Threading;
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Allows consumers to be notified of application lifetime events. This interface is not intended to be user-replaceable.
/// </summary>
public interface IHostApplicationLifetime
{
/// <summary>
/// Triggered when the application host has fully started.
/// </summary>
CancellationToken ApplicationStarted { get; }
/// <summary>
/// Triggered when the application host is starting a graceful shutdown.
/// Shutdown will block until all callbacks registered on this token have completed.
/// </summary>
CancellationToken ApplicationStopping { get; }
/// <summary>
/// Triggered when the application host has completed a graceful shutdown.
/// The application will not exit until all callbacks registered on this token have completed.
/// </summary>
CancellationToken ApplicationStopped { get; }
/// <summary>
/// Requests termination of the current application.
/// </summary>
void StopApplication();
}
}
可以看到,還提供了一個 StopApplication() 可以關閉應用程序。
IHostApplicationLifetime 的實現類 ApplicationLifetime
ApplicationLifetime 實現了接口 IHostApplicationLifetime,並提供了一些輔助方法。
在 GitHub 中查看 ApplicationLifetime 源碼
public class ApplicationLifetime : IApplicationLifetime, IHostApplicationLifetime {
private readonly CancellationTokenSource _startedSource = new CancellationTokenSource();
private readonly CancellationTokenSource _stoppingSource = new CancellationTokenSource();
private readonly CancellationTokenSource _stoppedSource = new CancellationTokenSource();
/// <summary>
/// Triggered when the application host has fully started and is about to wait
/// for a graceful shutdown.
/// </summary>
public CancellationToken ApplicationStarted => _startedSource.Token;
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// Request may still be in flight. Shutdown will block until this event completes.
/// </summary>
public CancellationToken ApplicationStopping => _stoppingSource.Token;
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// All requests should be complete at this point. Shutdown will block
/// until this event completes.
/// </summary>
public CancellationToken ApplicationStopped => _stoppedSource.Token;
public void StopApplication() {}
public void NotifyStarted() {}
public void NotifyStopped() {}
}
在實現中,這些方法用來發出生命周期的事件通知:
- NotifyStarted()
- NotifyStopped()
注冊生命周期管理服務
這兩個生命周期管理對象都是通過依賴注入注冊到容器中。以后,通過依賴注入獲取服務對象的時候,根據構造函數參數中的依賴描述,從容器中得到對象實例。
從下面的代碼中,可以看到 IApplicationLifetime 實際上與 IHostApplicationLifetime 引用的都是同一個對象實例。
它們都是單例的。在 HostBuilder 中被注冊到容器中。
private void CreateServiceProvider()
{
var services = new ServiceCollection();
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
services.AddSingleton(_hostBuilderContext);
// register configuration as factory to make it dispose with the service provider
services.AddSingleton(_ => _appConfiguration);
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IApplicationLifetime>(s =>
(IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
services.AddSingleton<IHost, Internal.Host>();
services.AddOptions();
services.AddLogging();
啟動服務 StartAsync
StartAsync 方法首先調用了 HostLifetime 的 WaitForStartAsync() 來注冊 Host 本身的生命周期管理。
然后注冊到 HostBuilder上下文中的 IHostedService 對象按照注冊時的先后順序執行IHostedService.StartAsync()
方法進行啟動。 將所有的IHostedService
對象執行啟動完畢后,通過應用程序生命周期管理對象IHostApplicationLifetime
通知應用程序,啟動就完成了。
public IServiceProvider Services { get; }
public async Task StartAsync(CancellationToken cancellationToken = default)
{
_logger.Starting();
using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping);
var combinedCancellationToken = combinedCancellationTokenSource.Token;
await _hostLifetime.WaitForStartAsync(combinedCancellationToken);
combinedCancellationToken.ThrowIfCancellationRequested();
_hostedServices = Services.GetService<IEnumerable<IHostedService>>();
foreach (var hostedService in _hostedServices)
{
// Fire IHostedService.Start
await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
}
// Fire IHostApplicationLifetime.Started
_applicationLifetime?.NotifyStarted();
_logger.Started();
}
停止服務 StopAsync()
StopAsync 方法與 StartAsync 方法基本類似,首先通過應用程序生命周期管理對象 IHostApplicationLifetime
通知應用程序即將開始停止服務,然后一次調用IHostedService
對象停止服務的運行,最后通知應用程序已完成結束方法。
public async Task StopAsync(CancellationToken cancellationToken = default)
{
_logger.Stopping();
using (var cts = new CancellationTokenSource(_options.ShutdownTimeout))
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken))
{
var token = linkedCts.Token;
// Trigger IHostApplicationLifetime.ApplicationStopping
_applicationLifetime?.StopApplication();
IList<Exception> exceptions = new List<Exception>();
if (_hostedServices != null) // Started?
{
foreach (var hostedService in _hostedServices.Reverse())
{
token.ThrowIfCancellationRequested();
try
{
await hostedService.StopAsync(token).ConfigureAwait(false);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
}
token.ThrowIfCancellationRequested();
await _hostLifetime.StopAsync(token);
// Fire IHostApplicationLifetime.Stopped
_applicationLifetime?.NotifyStopped();
if (exceptions.Count > 0)
{
var ex = new AggregateException("One or more hosted services failed to stop.", exceptions);
_logger.StoppedWithException(ex);
throw ex;
}
}
_logger.Stopped();
}
應用
這里面主要涉及到兩個生命周期管理,其中最為主要的是 IHostApplicationLifetime,通過上面的分析,我們可以看到它已經被默認注冊到依賴注入容器中,所以,我們可以在需要對應用的生命周期進行管理的時候,通過注入這個對象來獲得對應用程序生命周期的響應。