CancellationToken类有个容易被忽视的功能,那就是它包含一个Register()方法,这个方法可以注册一个委托,当这个CancellationToken类对象被Cancel时可以触发这个委托的执行。Register()可以被执行多次,表示注册了好几个委托,Cancel到来时,可以触发执行所有这些已注册的委托。
Asp.net Core的非泛型主机运用了这个原理进行生命周期管理。
生命周期管理指代码框架可以管理主机启动和停止(甚至停止中)的过程。用户可以很方便的嵌入自己的代码,对这些生命过程进行监控,比如我们想在启动时输出点信息。
Asp.net Core将主机分为泛型主机(Host)和应用主机服务(Application Host,我们也称非泛型主机),Web主机就是一个应用主机,应用主机也称为主机服务(必需实现IHostedService),总之:
下面解密下Host公开的StartAsync()方法内的源码:
public async Task StartAsync(CancellationToken cancellationToken = default)
{
_logger.Starting();
await _hostLifetime.WaitForStartAsync(cancellationToken);// 默认_hostLifetime就是ConsoleLifetime对象
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();// _applicationLifetime是ApplicationLifetime对象。
_logger.Started();
}
看这里的这个foreach语句,就是将hostedService里头的IHostedService主机服务挨个启动。hostedService则是通过DI(依赖注入)注入的。
Asp.net Core会注入一个IHostLifetime的类对象,并且这个对象是Singletone的,即它是全场唯一的。它是用来掌管泛型Host主机的生命周期的。除非我们想自己实现一个,否则我们可以不需要建这么个类对象,它默认注入的是ConsoleLifetime类对象,该类实现了IHostLifetime接口。
看一下内部代码ConsoleLifetime.cs片段(WaitForStartAsync()方法是IHostLifetime接口的方法):
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;
}
这里的第13-22行,是用来捕获程序退出事件的,包括在控制台下按了Ctrl+C,或者点击了控制台对话框右上角的×。注意,这里(15行,21行)退出的时候触发了ApplicationLifetime.StopApplication()的执行。这个方法则是被Host的StartAsync()方法所调用。
后面我们会看到ApplicationLifetime.StopApplication()的执行就会触发一系列CancellationToken注册的委托的执行,而这里的ApplicationLifetime则代表了非泛型主机(应用主机服务)的生命管理对象。
IHostApplicationLifetime原来叫IApplicationLifetime,微软将这个东西改名了,说实话,改名后的源码未找到,也不知道这是为什么。我们找不到IHostApplicationLifetime,但我们可以找到IApplicationLifetime,它包含一个叫做StopApplication()方法。我们先理解它就可以了。这接口它也是被依赖注入的,它是用来掌管其他非泛型主机生命周期的,并且它也是Singleton,即它也是独一份的,Asp.net Core框架默认注入的是ApplicationLifetime这个类对象。那么问题来了,既然我们可以注入无数个不同的非泛型主机,按道理一个非泛型主机包含一个生命管理周期对象,怎么就只有一个生命周期管理对象?这里的原因是,Asp.net Core将非泛型主机和IHostApplicationLifetime是分为两个独立的类,然后用一个IHostApplicationLifetime对象的注册机制(也就是刚开始谈到的CancellationToken的Register()方法)来掌管所有非泛型主机的生命周期。ApplicationLifetime类对象里包含三个CancellationToken类对象:
从这三个名字就可以知道分别是用来掌管启动完毕、停止中、停止完毕后三个生命周期。分别对这三个令牌执行Cancel取消来触发各自注册了的委托。所以说,这里Cancel根本不是本来的取消意思,仅仅是Asp.net Core的技术团队使用了CancellationToken类的这个特性而已,我自己想想感觉这个用法是巧妙,但是总感觉有点蛋疼。从这里,我们就可以推断出,如果自己注册的非泛型主机,想使用生命周期管理,那就可以在非泛型主机的构造函数中携带入这个IHostApplicationLifetime,然后我们只要在这里头为上面的三个令牌Regiseter()一下自己想要的委托。
现在我们看一下ApplicationLifetime的StopApplication()方法,这个方法是IHostApplicationLifetime的接口方法。这个方法里面实际上就是调用了令牌的Cancel()方法,触发一连串的Regiseter()委托的执行。
那么Asp.net Core框架又是在哪里触发StopApplication()方法的呢?我们再回顾IHostLifetime一节提到的代码片段的第13-22行,这里在捕获到程序退出时会触发ApplicationLifetime的StopApplication()方法。由此触发Stopping的生命过程比较清晰了。
ApplicationLifetime的StopApplication()方法可以触发Stopping委托。Started委托和Stopped的委托则是靠调用ApplicationLifetime的NotifyStarted()和NotifyStopped()方法达成的。NotifyStarted()在Host的StartAsync()方法中被调用。NotifyStopped()在Host的StopAsync()方法中被调用。NotifyStarted()方法前面已经贴过了,现在贴出Host的StopAsync()方法:
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 IApplicationLifetime.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 IApplicationLifetime.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();
}
现在新的问题来了,哪里调用了Host的StartAsync()方法和StopAsync()方法?这个答案在HostingAbstractionsHostExtensions.cs源码当中可以清晰的找到,贴出它的整个源码:
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Extensions.Hosting
{
public static class HostingAbstractionsHostExtensions
{
///
/// Starts the host synchronously.
///
///
public static void Start(this IHost host)
{
host.StartAsync().GetAwaiter().GetResult();
}
///
/// Attempts to gracefully stop the host with the given timeout.
///
///
/// The timeout for stopping gracefully. Once expired the
/// server may terminate any remaining active connections.
///
public static Task StopAsync(this IHost host, TimeSpan timeout)
{
return host.StopAsync(new CancellationTokenSource(timeout).Token);
}
///
/// Block the calling thread until shutdown is triggered via Ctrl+C or SIGTERM.
///
/// The running .
public static void WaitForShutdown(this IHost host)
{
host.WaitForShutdownAsync().GetAwaiter().GetResult();
}
///
/// Runs an application and block the calling thread until host shutdown.
///
/// The to run.
public static void Run(this IHost host)
{
host.RunAsync().GetAwaiter().GetResult();
}
///
/// Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered.
///
/// The to run.
/// The token to trigger shutdown.
public static async Task RunAsync(this IHost host, CancellationToken token = default)
{
using (host)
{
await host.StartAsync(token);
await host.WaitForShutdownAsync(token);
}
}
///
/// Returns a Task that completes when shutdown is triggered via the given token.
///
/// The running .
/// The token to trigger shutdown.
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();
}
}
}
改源码就是对Host的扩展。这里面有我们熟悉的Asp.net Core模板中的Run()方法,这个方法最后就会调用Host的StartAsync()方法。而Host的扩展方法WaitForShutdownAsync()则会调用StopAsync()。
这里Run()内部通过GetAwaiter().GetResult()可以将系统处于停等状态。它内部又利用了TaskCompletionSource的特性达到停等状态。
应该说利用CancellationToken类的特性来管理生命周期,是一种技巧。这种技巧对于我们外人来说感到生搬硬套。不去了解,确实很难把"取消"和生命管理过程联系在一起,但是微软技术团队就是这么直接的把它们粘在了一起。
IHostApplicationLifetime接口直接暴露有StopApplication()方法,用来触发Stopping委托执行。IHostApplicationLifetime的默认实现有额外的NotifyStarted()和NotifyStopped()方法,用来触发Started和Stopped委托。但是遗憾的是,这两个方法并不在IHostApplicationLifetime接口中,因此Host在使用ApplicationLifetime时需要类型转换(参考下方Host.cs代码的第5行),如果这样的话,是不是意味着用户无法替换IHostApplicationLifetime的实现了?经过试验,真的无法注入自定义的IHostApplicationLifetime,运行时会直接抛出异常。这不能算是一个Bug,感觉像是微软技术团队代码框架设计问题。好在我们一般是不需要自定义一个IHostApplicationLifetime。
public Host(IServiceProvider services, IApplicationLifetime 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));
}