Asp.net Core 主机生命周期的管理

1. 回顾CancellationToken

  CancellationToken类有个容易被忽视的功能,那就是它包含一个Register()方法,这个方法可以注册一个委托,当这个CancellationToken类对象被Cancel时可以触发这个委托的执行。Register()可以被执行多次,表示注册了好几个委托,Cancel到来时,可以触发执行所有这些已注册的委托。
  Asp.net Core的非泛型主机运用了这个原理进行生命周期管理。

2. 生命周期管理

  生命周期管理指代码框架可以管理主机启动和停止(甚至停止中)的过程。用户可以很方便的嵌入自己的代码,对这些生命过程进行监控,比如我们想在启动时输出点信息。

3. 泛型主机与应用类主机服务

  Asp.net Core将主机分为泛型主机(Host)和应用主机服务(Application Host,我们也称非泛型主机),Web主机就是一个应用主机,应用主机也称为主机服务(必需实现IHostedService),总之:

  • 泛型主机Host,它实现的是IHost接口,它好比是黑社会老大,其他的应用主机都归它管,它是全场唯一的。
  • 非泛型主机,即应用类主机服务,它必须实现IHostedService接口,它可以是多个的,它是由泛型主机Host启动的。最重要的Web 主机,其实就是非泛型主机之一。我们可以往泛型主机里注入自己想要注入的其他应用主机。asp.net core 3.1利用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(依赖注入)注入的。

4. IHostLifetime

  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则代表了非泛型主机(应用主机服务)的生命管理对象。

5. IHostApplicationLifetime与三个生命周期类型的委托

  IHostApplicationLifetime原来叫IApplicationLifetime,微软将这个东西改名了,说实话,改名后的源码未找到,也不知道这是为什么。我们找不到IHostApplicationLifetime,但我们可以找到IApplicationLifetime,它包含一个叫做StopApplication()方法。我们先理解它就可以了。这接口它也是被依赖注入的,它是用来掌管其他非泛型主机生命周期的,并且它也是Singleton,即它也是独一份的,Asp.net Core框架默认注入的是ApplicationLifetime这个类对象。那么问题来了,既然我们可以注入无数个不同的非泛型主机,按道理一个非泛型主机包含一个生命管理周期对象,怎么就只有一个生命周期管理对象?这里的原因是,Asp.net Core将非泛型主机和IHostApplicationLifetime是分为两个独立的类,然后用一个IHostApplicationLifetime对象的注册机制(也就是刚开始谈到的CancellationToken的Register()方法)来掌管所有非泛型主机的生命周期。ApplicationLifetime类对象里包含三个CancellationToken类对象:

  • ApplicationStarted,它可以触发Started委托,即非泛型主机启动完毕的委托;
  • ApplicationStopping,它可以触发Stopping委托,即非泛型主机开始停止的委托;
  • ApplicationStopped,它可以触发Stopped委托,即非泛型主机停止完毕后的委托;

  从这三个名字就可以知道分别是用来掌管启动完毕、停止中、停止完毕后三个生命周期。分别对这三个令牌执行Cancel取消来触发各自注册了的委托。所以说,这里Cancel根本不是本来的取消意思,仅仅是Asp.net Core的技术团队使用了CancellationToken类的这个特性而已,我自己想想感觉这个用法是巧妙,但是总感觉有点蛋疼。从这里,我们就可以推断出,如果自己注册的非泛型主机,想使用生命周期管理,那就可以在非泛型主机的构造函数中携带入这个IHostApplicationLifetime,然后我们只要在这里头为上面的三个令牌Regiseter()一下自己想要的委托。
  现在我们看一下ApplicationLifetime的StopApplication()方法,这个方法是IHostApplicationLifetime的接口方法。这个方法里面实际上就是调用了令牌的Cancel()方法,触发一连串的Regiseter()委托的执行。
  那么Asp.net Core框架又是在哪里触发StopApplication()方法的呢?我们再回顾IHostLifetime一节提到的代码片段的第13-22行,这里在捕获到程序退出时会触发ApplicationLifetime的StopApplication()方法。由此触发Stopping的生命过程比较清晰了。

6. 已启动与已停止的生命过程

  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
    {
        /// <summary>
        /// Starts the host synchronously.
        /// </summary>
        /// <param name="host"></param>
        public static void Start(this IHost host)
        {
            host.StartAsync().GetAwaiter().GetResult();
        }

        /// <summary>
        /// Attempts to gracefully stop the host with the given timeout.
        /// </summary>
        /// <param name="host"></param>
        /// <param name="timeout">The timeout for stopping gracefully. Once expired the
        /// server may terminate any remaining active connections.</param>
        /// <returns></returns>
        public static Task StopAsync(this IHost host, TimeSpan timeout)
        {
            return host.StopAsync(new CancellationTokenSource(timeout).Token);
        }

        /// <summary>
        /// Block the calling thread until shutdown is triggered via Ctrl+C or SIGTERM.
        /// </summary>
        /// <param name="host">The running <see cref="IHost"/>.</param>
        public static void WaitForShutdown(this IHost host)
        {
            host.WaitForShutdownAsync().GetAwaiter().GetResult();
        }

        /// <summary>
        /// Runs an application and block the calling thread until host shutdown.
        /// </summary>
        /// <param name="host">The <see cref="IHost"/> to run.</param>
        public static void Run(this IHost host)
        {
            host.RunAsync().GetAwaiter().GetResult();
        }

        /// <summary>
        /// Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered.
        /// </summary>
        /// <param name="host">The <see cref="IHost"/> to run.</param>
        /// <param name="token">The token to trigger shutdown.</param>
        public static async Task RunAsync(this IHost host, CancellationToken token = default)
        {
            using (host)
            {
                await host.StartAsync(token);

                await host.WaitForShutdownAsync(token);
            }
        }

        /// <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();
        }
    }
}

  改源码就是对Host的扩展。这里面有我们熟悉的Asp.net Core模板中的Run()方法,这个方法最后就会调用Host的StartAsync()方法。而Host的扩展方法WaitForShutdownAsync()则会调用StopAsync()。
  这里Run()内部通过GetAwaiter().GetResult()可以将系统处于停等状态。它内部又利用了TaskCompletionSource的特性达到停等状态。

7. 评论

  应该说利用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));
        }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值