为了账号安全,请及时绑定邮箱和手机立即绑定

.NET托管服务深度解析

揭开云服务的内部机制(C#)

来源:NEOM

在 .NET 中,BackgroundService 类是用于实现长时间运行的服务的基础类,这些服务运行在托管主机环境中。资深开发者应熟悉此类的内部机制及其与 Host 类的交互情况。这种知识有助于有效管理和优化后台任务的执行及其生命周期。让我们深入内部,一起探索 dotnet/runtime 代码。

后台服务的内部结构:

BackgroundService 类用于创建长时间运行的托管服务,通过实现 IHostedService 接口。接下来我们来看看它的核心属性和方法:

属性

  • _executeTask:保存代表后台任务的任务,使服务能够跟踪任务的执行。
  • _stoppingCts:使用 CancellationTokenSource 处理取消请求,以便能够确保有序的关闭。

下面是 BackgroundService 内部实现的重要部分,展示了这些字段如何组织以管理任务的执行和取消。

https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs GitHub上的 Microsoft.Extensions.Hosting.Abstractions 源代码路径

    // .NET 运行时中的 BackgroundService 类
    public abstract class BackgroundService : IHostedService, IDisposable
    {
        private Task? _executeTask;
        private CancellationTokenSource? _stoppingCts;

        /// <summary>
        /// 获取后台操作执行任务。
        /// </summary>
        /// <remarks>
        /// 如果后台操作尚未启动,将返回 null。
        /// </remarks>
        public virtual Task? ExecuteTask => _executeTask;

        /// <summary>
        /// 执行异步后台操作。
        /// </summary>
        protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
    }

ExecuteAsync 是定义后台服务中长运行任务的核心方法。任何从 BackgroundService 继承来的类,都得实现这个抽象方法。

启动异步后台任务

StartAsync 是主机调用来初始化服务的过程的方法。它设置了 CancellationTokenSource,将主机提供的取消令牌与内部的 _stoppingCts 令牌关联起来,使服务能够协调来自两个来源的关闭流程。这种关联是单向的,即如果主机取消了它的令牌,那么相关的令牌也会被取消,允许服务处理外部关闭信号。然而,如果服务取消了其关联的令牌,那么不会影响主机的令牌,确保主机不会受到内部取消的影响。这确保服务在处理这两种情况时不会影响整个应用程序的运行。

    // .NET 运行时中的 BackgroundService 类
    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // 创建与提供的令牌关联的取消令牌,以便可以通过提供的取消令牌取消正在执行的任务
        _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        // 存储正在执行的任务
        _executeTask = ExecuteAsync(_stoppingCts.Token);

        // 如果任务已经完成,则返回该任务,这将把取消和失败的信息传递给调用者
        if (_executeTask.IsCompleted)
        {
            return _executeTask;
        }

        // 否则任务仍在运行
        return Task.CompletedTask;
    }

如果 ExecuteAsync 立即完成执行,StartAsync 将返回该任务本身,从而将任何取消请求或异常传递给调用方。对于长时间运行的任务,它返回 Task.CompletedTask,这样主机就能继续执行其他启动任务而不必等待它完成。

异步操作的优雅停止:支持取消的优雅停机

在托管服务中,主机在应用程序关闭期间会调用 StopAsync 方法,以启动后台任务的受控停止。此方法通过提供取消令牌来确保正在运行的后台任务能够优雅地完成,这些取消令牌会通知任务何时停止。

当调用 StopAsync 时,它会检查是否有任务正在运行,如果有,则通过调用 _stoppingCts.Cancel() 来取消该任务。这种取消机制会通知 ExecuteAsync 停止执行。在 ExecuteAsync 中使用此令牌进行任何异步操作,可以使任务平滑停止,帮助应用程序干净利落地完成工作。

    // .NET 中的 BackgroundService 类  
    public virtual async Task StopAsync(CancellationToken cancellationToken)  
    {  
        // 没有调用 Start 就调用了 Stop  
        if (_executeTask == null)  
        {  
            return;  
        }  

        try  
        {  
            // 通知正在执行的方法取消操作  
            _stoppingCts!.Cancel();  
        }  
        finally  
        {  
    #if (NET8_0_OR_GREATER)  
            await _executeTask.WaitAsync(cancellationToken).ConfigureAwait(false);  
    #else  
            // 任务完成或停止令牌生效时  
            定义一个任务完成源对象:tcs = new TaskCompletionSource<object>();  

            注册一个取消令牌的处理程序:  
            using CancellationTokenRegistration registration =   
    cancellationToken.Register(s =>   
    ((TaskCompletionSource<object>)s!).SetCanceled(), tcs);  
            等待任一任务完成或停止令牌生效:await Task.WhenAny(_executeTask, tcs.Task).ConfigureAwait(false);  
    #endif  
        }  
    }

如果取消令牌被忽略了或没有传递给某些异步方法,则后台任务可能会继续运行。默认情况下,.NET 会为后台任务提供 30 秒的时间来结束。如果需要更多时间,可以通过调整 ConfigureServices 来延长超时时间,以便任务可以优雅地完成。

    ConfigureServices((host, services) =>  
    {  
        services.Configure<HostOptions>(options =>   
        {    
            options.ShutdownTimeout = TimeSpan.FromSeconds(120);  // 设置主机选项的关闭超时时间为120秒  
        });  
    })
主机类互动:协调服务生命周期

在前面的部分里,我们已经讨论过 BackgroundServiceStartAsync 方法。接下来,我们来看看 Host 类是如何使用其 StartAsync 方法的。启动时,HostStartAsync 会调用每个 IHostedService 的启动方法,以确保后台服务准备就绪。

Host 类是如何启动每个服务的,如下:

    // .NET 运行时中的 Host 类
    public async Task StartAsync(CancellationToken cancellationToken = default)
    {  
        _logger.Starting();  
        _hostedServices ??= Services.GetRequiredService<IEnumerable<IHostedService>>();  

        // 对每个 IHostedService 调用 StartAsync()  

       await ForeachService(_hostedServices, cancellationToken, concurrent:   
    true, abortOnFirstException: false, new List<Exception>(),   
        async (service, token) => {  
            await service.StartAsync(token).ConfigureAwait(false);  
        }).ConfigureAwait(false);  

        _logger.Started();  
    }
主机管理 "Lifetime" 的方式
了解 IHostApplicationLifetime

应用程序生命周期托管接口 可通过默认的依赖注入容器获得,提供了三个关键的 CancellationToken 事件来管理应用程序生命周期中的关键点。

    // 本文件已根据一个或多个协议授权给 .NET Foundation。  
    // .NET Foundation 按照 MIT 许可证向您授予此文件的使用权。  

    using System.Threading;  

    命名空间 Microsoft.Extensions.Hosting {  
        /// <summary>  
        /// 允许消费者接收有关应用程序生命周期事件的通知。此接口不建议用户替换。  
        /// </summary>  
        公共接口 IHostApplicationLifetime {  
            CancellationToken ApplicationStarted { get; }  
            CancellationToken ApplicationStopping { get; }  
            CancellationToken ApplicationStopped { get; }  

            /// <summary>停止应用程序。</summary>  
            void StopApplication();  
        }  
    }
  • ApplicationStarted : 主机启动完成时触发,表示所有服务已经启动。
  • ApplicationStopping : 开始优雅关闭时触发,允许服务开始释放资源。
  • ApplicationStopped : 主机关闭完成时触发,确保所有清理工作已经结束。

当遇到以下情况时,注册方法到事件可能会有所帮助:

  • 在应用程序启动或停止时,精确执行相应的操作。
  • 协调初始化和清理工作,这些工作依赖于应用程序的整体状态。
  • 确保资源管理与应用程序的整个生命周期相匹配。

这对于处理资源密集型任务,或确保所有依赖项在执行前已经准备好特别有帮助。

停止应用程序(StopApplication())方法:实现控制关闭

通过使用StopApplication,我们可以在出现严重问题时程序化地启动关闭过程,确保整个应用程序有一个受控的关闭过程。比如,当一个BackgroundService遇到严重错误时,StopApplication能够通知主机优雅地停止所有服务。

    private readonly IHostApplicationLifetime _appLifetime;  

    public MyService(IHostApplicationLifetime appLifetime)  
    {  
        _appLifetime = appLifetime;  
    }  

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        try  
        {  
            // 主要的处理逻辑  
        }  
        catch (Exception ex)  
        {  
            // 捕获异常并记录必要日志  
        }  
        finally  
        {  
            _appLifetime.StopApplication();  
        }  
    }  

这样做:

  • 如果发生异常,则在 catch 块中记录异常,并在 finally 块中通过 StopApplication 触发受控关闭。这确保了任何致命问题都能引发平稳的退出过程,防止任务在不稳定状态下继续执行。
  • StopApplication 允许服务按照定义的顺序停止,通过确保依赖服务完成后再关闭,防止资源泄漏或数据不一致的问题。

在主服务出现严重错误、关键依赖失败,或在云环境中优雅地关闭应用程序更有利于干净重启时,使用 StopApplication。这种方法有助于系统快速恢复,减少停机时间,并确保下次运行时系统稳定。

https://learn.microsoft.com/zh-cn/dotnet/core/extensions/generic-host?tabs=appbuilder

启动与关闭的优化顺序:启动与关闭

在 .NET 中,托管服务注册的顺序对启动和关闭行为有很大影响。默认情况下,宿主会依注册顺序依次调用每个 IHostedServiceStartAsync 方法。每个服务必须完全启动后,下一个服务才能开始启动,这在有些服务需要先初始化其他服务时特别重要。

对于独立服务,.NET 提供了并行启动的选项,允许所有服务同时启动,。然而,如果需要特定顺序的话,保持默认的顺序启动顺序提供了更精确的控制能力。

逆向关机顺序

注册顺序不仅管理启动流程,还决定了关闭顺序。在关闭时,主机按照注册顺序的相反顺序调用每个服务的 StopAsync 方法。这意味着最后注册的服务最先停止,而最先注册的服务则最后停止。

调整注册的顺序可以让关机更顺畅。通过提前注册依赖服务并推迟注册供应商,这样可以确保任务按正确的顺序停止。这可以减少在关机时添加新任务的风险,从而使系统更高效地完成工作。

何时使用并发停止

对于具有许多独立服务的应用程序,依次关闭可能会比必要的时间更长。改为同时关闭可以让服务同时停止,从而缩短总的关闭时间。然而,对于那些需要严格控制关闭顺序的系统,依次关闭模式则更为合适。

你好,我是 Pawel,一名来自瑞士苏黎世的 .NET 开发人员。请查看我的 GitHub 项目:一个实用的 .NET 8 清净架构示例。该项目展示了结合 IdentityServer 和 EF Core 仓储模式的实用干净架构方法。点击这里查看:GitHub 实用干净架构。谢谢您的关注!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消