怪奇物语

怪奇物语

首页 新随笔 联系 管理

.NET 宿主与后台服务:深入理解 BackgroundService 与生命周期令牌

在 .NET 生态中,构建长期运行的服务(如后台任务、定时任务、消息消费者等)时,IHost(宿主)和 BackgroundService 是两个核心组件。它们不仅能帮助我们管理服务的生命周期,还能通过 CancellationToken 实现服务的优雅启动与停止。本文将通过一个完整示例,带你深入理解这些概念的实际应用。

一、什么是宿主(IHost)与后台服务(BackgroundService

  • 宿主(IHost:是 .NET 应用的“容器”,负责管理服务的启动、运行、停止等生命周期,同时整合了配置、日志、依赖注入等核心功能。无论是控制台应用、Windows 服务还是 Linux 守护进程,都可以基于宿主实现。

  • 后台服务(BackgroundService:是 IHostedService 接口的抽象实现,专门用于编写长期运行的后台任务(如定时任务、流数据处理等)。它通过 ExecuteAsync 方法定义任务逻辑,并通过 CancellationToken 响应宿主的停止信号。

二、实战示例:一个跟踪令牌状态的后台服务

我们通过一个完整示例,展示宿主如何管理后台服务,以及 CancellationToken(生命周期令牌)在服务停止时的作用。

1. 项目配置(.csproj

首先需要引入宿主相关的依赖包。在 .csproj 中添加:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <!-- 宿主与后台服务核心依赖 -->
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
  </ItemGroup>
</Project>

Microsoft.Extensions.Hosting 包包含了 IHostBackgroundService 等核心类型,是构建宿主应用的基础。

2. 后台服务实现(TokenDemoService.cs

我们定义一个 TokenDemoService 继承 BackgroundService,用于跟踪生命周期令牌的状态变化,并在服务停止时执行清理逻辑:

using Microsoft.Extensions.Hosting;

public class TokenDemoService : BackgroundService
{
    private int _count = 0;

    // 核心方法:后台任务的执行逻辑
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine($"[服务启动] 初始令牌状态:是否取消 = {stoppingToken.IsCancellationRequested}");

        try
        {
            // 循环执行任务,直到令牌被取消
            while (!stoppingToken.IsCancellationRequested)
            {
                _count++;
                Console.WriteLine($"[运行中] 第 {_count} 次执行,令牌状态:是否取消 = {stoppingToken.IsCancellationRequested}");
                // 延迟1秒(传递令牌,若取消会立即抛出异常)
                await Task.Delay(1000, stoppingToken);
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // 仅处理当前令牌的取消异常(过滤其他来源的取消)
            Console.WriteLine($"[异常捕获] 检测到令牌取消,准备清理...");
        }

        // 无论正常退出还是异常取消,都执行清理逻辑
        Console.WriteLine($"[服务退出] 令牌已取消(状态:{stoppingToken.IsCancellationRequested}),开始清理资源...");
        await CleanupAsync();
        Console.WriteLine($"[服务退出] 清理完成,服务终止");
    }

    // 模拟退出前的资源清理(如释放连接、关闭文件等)
    private async Task CleanupAsync()
    {
        await Task.Delay(1000); // 模拟清理耗时
    }
}

关键逻辑解析:

  • stoppingToken 的作用:这是宿主传递的生命周期令牌,用于通知后台服务“宿主即将停止”。其 IsCancellationRequested 属性会在宿主停止时从 false 变为 true

  • 循环执行任务while (!stoppingToken.IsCancellationRequested) 确保任务在宿主运行期间持续执行,直到令牌被取消。

  • Task.Delay 与令牌await Task.Delay(1000, stoppingToken) 会在令牌取消时立即抛出 OperationCanceledException,避免任务在“已取消”状态下继续等待。

  • 异常处理:通过 try/catch 捕获 OperationCanceledException,并使用 when (stoppingToken.IsCancellationRequested) 筛选器确保只处理当前令牌的取消(忽略其他来源的取消信号)。

  • 清理逻辑:无论正常退出循环还是因异常中断,都会执行 CleanupAsync 方法,确保资源(如数据库连接、网络流)被正确释放。

3. 宿主配置与启动(Program.cs

宿主负责注册后台服务、启动服务、并在需要时触发停止流程。以下是 .NET 7+ 和 .NET 7 之前的两种常见写法:

.NET 7+ 简洁写法(推荐)

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public class Program
{
    public static async Task Main(string[] args)
    {
        // 创建宿主构建器(.NET 7+ 新API,整合配置、服务、环境等)
        HostApplicationBuilder hostBuilder = Host.CreateApplicationBuilder();
        
        // 注册后台服务
        hostBuilder.Services.AddHostedService<TokenDemoService>();
        
        // 构建宿主
        IHost host = hostBuilder.Build();

        // 启动宿主(非阻塞,服务在后台运行)
        _ = host.StartAsync();
        Console.WriteLine("宿主已启动,按任意键触发服务停止...");

        // 等待用户输入,模拟“主动终止服务”
        Console.ReadKey();
        Console.WriteLine("用户触发停止,宿主开始执行停止流程...");

        // 触发宿主停止(会自动设置stoppingToken为取消状态)
        await host.StopAsync();
        Console.WriteLine("宿主已完全停止");
    }
}

.NET 7 之前的传统写法

// 传统链式配置写法
IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<TokenDemoService>();
    })
    .Build();

两种写法的区别:

  • Host.CreateApplicationBuilder()(.NET 7+):将配置、服务、环境等功能整合到一个 HostApplicationBuilder 对象中,通过属性直接操作(如 hostBuilder.ServiceshostBuilder.Configuration),更简洁直观。

  • Host.CreateDefaultBuilder(args)(.NET 3.0+):通过链式 Configure* 方法(如 ConfigureServices)配置宿主,适合复杂场景下的分步配置。

三、运行结果与生命周期解析

运行程序后,输出如下(关键节点已标注):

[服务启动] 初始令牌状态:是否取消 = False  // 1. 服务启动,令牌未取消
[运行中] 第 1 次执行,令牌状态:是否取消 = False
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
宿主已启动,按任意键触发服务停止...
[运行中] 第 2 次执行,令牌状态:是否取消 = False
[运行中] 第 3 次执行,令牌状态:是否取消 = False
(用户按任意键)
用户触发停止,宿主开始执行停止流程...
[异常捕获] 检测到令牌取消,准备清理...  // 2. 令牌取消,捕获异常
[服务退出] 令牌已取消(状态:True),开始清理资源...  // 3. 执行清理
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
[服务退出] 清理完成,服务终止  // 4. 清理完成,服务终止
宿主已完全停止

生命周期完整流程:

  1. 启动阶段:宿主启动后,TokenDemoServiceExecuteAsync 方法被调用,stoppingToken.IsCancellationRequestedfalse,任务进入循环执行状态。

  2. 运行阶段:每秒输出一次“运行中”日志,Task.Delay 等待期间会监听令牌状态。

  3. 停止阶段:用户触发 host.StopAsync() 后,宿主会将 stoppingToken 标记为“已取消”(IsCancellationRequested = true):

    • 若此时正在执行 Task.Delay,会立即抛出 OperationCanceledException,被 catch 块捕获。
    • 循环退出,执行后续的清理逻辑(CleanupAsync)。
  4. 终止阶段:清理完成后,服务正式终止,宿主输出“已完全停止”。

四、常见问题与注意事项

  1. 清理逻辑不执行?
    若未捕获 Task.Delay 抛出的 OperationCanceledException,会导致程序直接退出 ExecuteAsync 方法,跳过清理逻辑。务必通过 try/catch 确保清理代码执行。

  2. 令牌取消的来源?
    stoppingToken 的取消信号来自宿主,可能的触发场景包括:用户主动停止(如 Ctrl+C)、调用 host.StopAsync()、服务异常崩溃等。

  3. 超时处理?
    宿主默认等待后台服务 5 秒以完成清理,超时后会强制终止。可通过 ConfigureHostOptions 调整超时时间:

    hostBuilder.ConfigureHostOptions(options =>
    {
        options.ShutdownTimeout = TimeSpan.FromSeconds(10); // 延长至10秒
    });
    

五、总结

  • 宿主(IHost 是服务的“管理者”,负责生命周期控制和资源整合。
  • BackgroundService 是编写后台任务的便捷基类,通过 ExecuteAsync 定义核心逻辑。
  • CancellationTokenstoppingToken 是宿主与后台服务的“通信桥梁”,用于通知服务停止,确保优雅退出。
  • 务必通过 try/catch 处理取消异常,并在退出前执行资源清理,避免内存泄漏或资源占用。

掌握这些概念后,你可以更规范地构建 .NET 后台服务,无论是简单的定时任务还是复杂的分布式服务,都能实现可靠的生命周期管理。

代码示例

BackgroundDemo\Program.cs


using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public class Program
{
    public static async Task Main(string[] args)
    {
        // 创建宿主,注册后台服务
        // ------------------------------.net7之后版本的写法,简洁----------------------------------------------
        HostApplicationBuilder hostApplicationBuilder = Host.CreateApplicationBuilder();
        hostApplicationBuilder.Services.AddHostedService<TokenDemoService>();
        IHost host = hostApplicationBuilder.Build();
        // -----------------------------------or.net7之前版本的写法-----------------------------------------
        // IHost host = Host.CreateDefaultBuilder(args)
        //     .ConfigureServices(services =>
        //     {
        //         services.AddHostedService<TokenDemoService>();
        //     })
        //     .Build();
        // ----------------------------------------------------------------------------

        // 启动宿主(非阻塞)
        _ = host.StartAsync();
        Console.WriteLine("宿主已启动,按任意键触发服务停止...");

        // 等待用户输入,模拟“主动终止服务”
        Console.ReadKey();
        Console.WriteLine("用户触发停止,宿主开始执行停止流程...");

        // 触发宿主停止(会自动设置stoppingToken为取消状态)
        await host.StopAsync();
        Console.WriteLine("宿主已完全停止");
    }
}

BackgroundDemo\TokenDemoService.cs


using Microsoft.Extensions.Hosting;

public class TokenDemoService : BackgroundService
{
    private int _count = 0;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine($"[服务启动] 初始令牌状态:是否取消 = {stoppingToken.IsCancellationRequested}");

        try
        {
            // 循环执行任务,直到令牌被取消
            while (!stoppingToken.IsCancellationRequested)
            {
                _count++;
                Console.WriteLine($"[运行中] 第 {_count} 次执行,令牌状态:是否取消 = {stoppingToken.IsCancellationRequested}");
                await Task.Delay(1000, stoppingToken); // 延迟可能抛出异常
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // 仅处理当前令牌的取消异常(忽略其他来源的取消)
            Console.WriteLine($"[异常捕获] 检测到令牌取消,准备清理...");
        }

        // 无论正常退出还是异常取消,都执行清理
        Console.WriteLine($"[服务退出] 令牌已取消(状态:{stoppingToken.IsCancellationRequested}),开始清理资源...");
        await CleanupAsync();
        Console.WriteLine($"[服务退出] 清理完成,服务终止");
    }

    private async Task CleanupAsync()
    {
        await Task.Delay(1000); // 模拟清理耗时
    }
}


BackgroundDemo\BackgroundDemo.csproj


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>

  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
  </ItemGroup>



</Project>

posted on 2025-10-27 08:00  超级无敌美少男战士  阅读(7)  评论(0)    收藏  举报