在桌面应用中使用 .NET Generic Host 与 BackgroundService 的理由

· · C#, .NET, Generic Host, BackgroundService, WPF, WinForms, Windows开发, 设计

Windows 工具或常驻类应用稍微发展壮大后,UI 之外的处理会逐渐增多。 定期轮询、文件监控、重新连接、队列处理、启动时初始化、退出时 flush。 一开始靠 Form_LoadOnStartupTask.Run 还能应付,但继续这样发展下去,谁来启动、谁来停止、谁来监视异常就会变得模糊不清。

在考虑 async / await 的写法之前,更应该先决定谁来掌管处理的生命周期——这正是需要 .NET 的 Generic Host 与 BackgroundService 发挥作用的场景。

关于 UI 线程侧的 async / await,可以参考 用一张图整理 WPF / WinForms 的 async/await 与 UI 线程 —— await 之后返回到哪里、Dispatcher、ConfigureAwait、.Result / .Wait() 的卡点C# async/await 最佳实践 - Task.Run 与 ConfigureAwait 的判断表 这两篇文章。 本文将聚焦于更外层的「整个应用的启动与停止」整理。

在实际项目中,容易慢慢腐化的大致是下面这些地方。

  • 表单或 ViewModel 的各个角落里冒出 Task.Run
  • 常驻循环的停止条件用 bool 标志散落各处
  • 退出时还有处理在运行,偶尔关不干净
  • 日志 / 配置 / DI 的入口按技术栈各自为政
  • 想用 Environment.Exit 图个省事,结果 finally 被跳过

本文主要以 .NET 6 及以后版本的 WPF / WinForms / 常驻类 Windows 应用为前提,整理 Generic Host / BackgroundService 为什么会悄然发挥作用、引入到什么程度比较划算、以及在哪里偷懒就会陷入泥潭。

另外,本文中出现的代码已作为可构建、可运行的完整示例(库、演示从启动到 graceful shutdown 的控制台 demo、单元测试)发布在 GitHub 上。

generic-host-backgroundservice-desktop-app - komurasoft-blog-samples (GitHub)

先统一术语

这类话题如果术语含义模糊,会突然变得难以理解。 所以先大致固定一下本文使用的词汇。

  • Generic Host
    • 统一负责 .NET 应用「启动」「依赖关系」「配置」「日志」「停止」的基础设施。
    • 不仅是 ASP.NET Core 专属机制,控制台、worker、桌面应用也都能使用。
  • Host / IHost
    • build 之后得到的实体。
    • 通过 StartAsync 启动它,通过 StopAsync 停止它。
  • Hosted Service
    • 随 host 的生命周期一起启动和停止的常驻处理。
    • 可以实现 IHostedService,但通常是继承 BackgroundService 来编写。
  • BackgroundService
    • IHostedService 的一个便于编写的辅助实现。
    • 可以把长时间运行的主体写在 ExecuteAsync 中,便于整理监控循环或定期处理。
  • lifetime(生命周期)
    • 本文中指「该处理何时开始、何时结束、由谁负责停止」的含义。
    • 与单纯的存活时间不同,它包含了启动职责与停止职责的寿命管理。
  • graceful shutdown(平滑关闭)
    • 不是强制终止,而是发出停止信号,尽量整理好正在进行的处理之后再结束。
    • 例如「不再开始下一个周期」「决定队列处理到哪里为止」「等待 close 或 flush 完成」等都属于这个范畴。
  • DI
    • Dependency Injection(依赖注入)的缩写,指不在调用方硬编码依赖对象的组装方式,而是通过容器来获取。
    • 在本文中,理解为「不把 logger、配置、reader 到处 new,而是在入口统一配置」就足够了。

本文的内容不止是「介绍 BackgroundService 这个方便的类」, 不如把它理解为 把整个应用的启动与停止都交给 host 统一管理、把常驻处理的 lifetime 作为一种设计来对待的话题, 这样阅读起来会更容易跟上。

目录

  1. 先说结论(一句话)
  2. 先用一张图整理
    • 2.1. 整体概览
    • 2.2. 放置位置判断表
  3. 为什么在桌面应用中会有效
    • 3.1. 容易将 UI 与常驻处理的职责分开
    • 3.2. 可以把启动、停止、异常的入口集中到一处
    • 3.3. 容易把 graceful shutdown 纳入设计
    • 3.4. DI / 日志 / 配置从一开始就齐备
  4. 适合的场景
  5. 最小配置示例(WPF 示例)
  6. StartAsync / ExecuteAsync / StopAsync 的划分方式
    • 6.1. StartAsync
    • 6.2. ExecuteAsync
    • 6.3. StopAsync
    • 6.4. .NET 10 以后的注意事项
  7. 常见的反模式
  8. 代码评审时的检查清单
  9. 简要的使用区分
  10. 总结
  11. 参考资料

1. 先说结论(一句话)

  • Generic Host 即使在桌面应用中,也是非常有力的 启动与 lifetime 管理基础设施
  • BackgroundService 是一个容器,能把「长期存活的处理」纳入 受管理的生命周期,而不是丢给 Task.Run 就不管了。
  • 在实际工作中最有效的一点,是能把 启动职责 / 停止职责 / 异常监控 / 日志 / DI / 配置 都集中到同一处设计中。
  • StartAsync 写得简短,长期运行的主体放进 ExecuteAsync,退出时的收尾工作放进 StopAsync,这样划分会让代码相当易读。
  • 常驻应用、托盘应用、设备监控、定期同步、有序的后处理、重新连接循环,与这种做法特别相配。
  • 反过来,如果把只在点击按钮时执行一次的处理也都做成 BackgroundService,就会显得有点大动干戈。
  • StopAsync 虽然方便,但并不是进程崩溃或强制终止时的保险。不要把收尾工作过度依赖于它,这点也很重要。

归根结底,Generic Host / BackgroundService 在桌面应用中之所以有效, 与其说是「因为存在后台处理」, 不如说是「希望把这些后台处理的生命周期作为一种设计来持有,而不是当作 UI 的附属品」。

2. 先用一张图整理

2.1. 整体概览

先看这张图,理解起来会快很多。

桌面应用启动(WPF / WinForms)构建 Host / StartAsync准备 DI / Logging / ConfigurationHostedService.StartAsyncBackgroundService.ExecuteAsyncPeriodicTimer / 队列 / 重新连接 / 监控循环显示 MainWindow / MainForm状态更新 / 日志 / 外部 I/OUI 只在需要的地方使用 Dispatcher / Invoke用户退出 / 致命错误 / StopApplicationIHost.StopAsyncCancellationToken 通知HostedService.StopAsync连接 close / flush / graceful shutdown

UI 应用中常见的情况是,职责会一点点分散到 Program.cs / App.xaml.cs / Form_Load / Closing / Task.Run / Timer / static 单例中。

引入 Host 之后,可以大致划分为以下职责。

  • UI:界面、输入、显示
  • HostedService / BackgroundService:常驻处理、监控、队列处理、定期处理
  • DI 服务:实际的业务逻辑、外部连接、配置、日志

只要能做到这种划分,代码评审的难易度就会有很大改善。

2.2. 放置位置判断表

想做的事 首选放置位置 理由
启动后紧接着的轻量初始化 StartAsync 作为参与启动的短处理,意义明确
长期存活的监控 / 轮询 / 重新连接 ExecuteAsync 便于与服务生命周期一起运行
退出时的停止通知 / flush / close StopAsync 便于结合 CancellationToken 编写 graceful shutdown
依赖关系的组装、配置、日志 Host.CreateApplicationBuilder 可以把入口集中到一处
界面更新 UI 侧 不让 worker 直接操作 UI 更不容易出问题
每次点击按钮执行一次的处理 普通的 async 方法 很多情况下不需要做成 HostedService
有序的后台后处理 Channel<T> + BackgroundService 比起随手抛出去,更容易管理生命周期和上限

引入 Host 的价值,与其说是能把某些操作「变成异步」, 不如说在于让「应该放在哪里」这个判断变得清晰明确

3. 为什么在桌面应用中会有效

3.1. 容易将 UI 与常驻处理的职责分开

桌面应用看起来 UI 是主角,但在实际项目中变得沉重的部分,大多是 UI 之外的东西。

例如:

  • 每 10 秒进行一次状态同步
  • 与设备或服务器的重新连接
  • 文件监控与导入
  • 队列中堆积的后处理
  • 日志转发或指标发送
  • 启动时的缓存 warm-up

这些并不是「界面事件」,而是挂在整个应用生命周期上的处理

如果把这些放在表单或窗口的 code-behind 里, 关闭界面时的停止职责、 捕获异常的职责、 决定重试或 backoff 的职责, 就会开始和 UI 的事情混在一起。

使用 BackgroundService,就能以代码的形式明确声明: 「这个处理会在应用运行期间一直存活」。 这一点看似不起眼,其实相当有力。

3.2. 可以把启动、停止、异常的入口集中到一处

即使不使用 Host 的桌面应用,只要单独排列 ServiceCollectionConfigurationBuilderLoggerFactory,也能做出类似的效果。

只是这种形式大多会渐渐分散开来。

  • DI 放在 Program.cs
  • 配置用自己写的 static
  • 日志用另一个 factory
  • 退出处理放在 ApplicationExit
  • 常驻处理用 Task.Run

这种状态一开始也能正常运行。 但几个月后再回头看,就很难看出到底是谁在掌管应用的生命周期

使用 Generic Host 之后,

  • 服务注册
  • 配置读取
  • 日志配置
  • hosted service 的启动
  • 停止通知
  • 通过 IHostApplicationLifetime 实现整体停止

都会纳入同一个框架中。

也就是说,「这个应用如何启动、如何停止」的入口很容易集中到一处。 对于常驻类应用来说,这一点会在后期发挥作用。

3.3. 容易把 graceful shutdown 纳入设计

常驻处理,停止比启动更难。 真的是这样。启动可能只需要三行代码,但结束往往一言难尽。

例如在退出时:

  • 想取消正在进行的 I/O
  • 想让下一个周期不再启动
  • 想决定队列中剩余任务处理到什么程度
  • 想关闭 socket 或 COM 对象
  • 想等待日志 flush 或状态保存完成

如果把这些都塞进 FormClosing,就会和界面的各种情况混在一起,变得很棘手。

如果使用 Host / BackgroundService,由于有 CancellationTokenStopAsync, 「用于停止的通道」从一开始就存在。

当然这不是魔法。 在崩溃或被强制 kill 的情况下,StopAsync 也可能不会被调用。 即便如此,只要有「正常退出时通过这条路径停止」这样的设计,情况就会安稳得多。

3.4. DI / 日志 / 配置从一开始就齐备

Generic Host 的好处不仅在于 BackgroundService

  • 通过 Host.CreateApplicationBuilder,DI / 配置 / 日志的基础一次性齐备
  • 可以直接方便地使用 appsettings.json 或环境变量
  • UI 和 worker 都能以同样的方式使用 ILogger<T>
  • 需要时可以用 IOptions<T> 系列把配置整理到一起

尤其是在 Windows 工具类项目中, 「一开始规模很小,就随手用 static 保存配置和 logger,后来却变得很痛苦」 这种情况相当常见。

如果从一开始就把这些交给 host 管理, 等应用稍微变得庞大时,就不容易喘不过气来。

4. 适合的场景

Generic Host / BackgroundService 特别容易发挥作用的场景如下。

  • 托盘常驻应用 有定期同步、监控、通知、重新连接
  • 设备 / 摄像头 / socket 连接应用 有连接维持、监控、重试、状态获取
  • 文件对接工具 有监控、导入队列、有序处理
  • 预防企业内部工具的臃肿化 一开始规模小,但配置、日志、外部 I/O 有可能增多
  • 对退出质量要求较高的应用 不希望在关闭时留下半途而废的状态

反过来,也有一开始不必急着引入 host 的情况。

  • 单次启动、只处理一次就结束的小工具
  • 几乎没有后台处理、仅靠 UI 事件就能完成的界面
  • 依赖关系或配置几乎不会增加的、真正很小的企业内部辅助工具

Host 并非「必需品」。 不过,一旦看到有两个以上的常驻处理,就完全值得积极考虑引入它。 比起事后清理满地的 Task.Run 殖民地,这样做的成本要低得多。

5. 最小配置示例(WPF 示例)

作为示例,下面写一个在 WPF 中启动 host、并运行每 5 秒读取一次外部状态的 BackgroundService 的最小配置。 在 WinForms 中,只是入口变成 Main / ApplicationContext,思路基本相同。

5.1. App.xaml.cs

using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DesktopHostSample;

public partial class App : Application
{
    private IHost? _host;

    protected override async void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        HostApplicationBuilder builder = Host.CreateApplicationBuilder(e.Args);

        builder.Services.Configure<HostOptions>(options =>
        {
            options.ShutdownTimeout = TimeSpan.FromSeconds(15);
        });

        builder.Services.AddSingleton<MainWindow>();
        builder.Services.AddSingleton<StatusStore>();
        builder.Services.AddScoped<IDeviceStatusReader, DeviceStatusReader>();
        builder.Services.AddHostedService<DevicePollingBackgroundService>();

        _host = builder.Build();

        await _host.StartAsync();

        MainWindow mainWindow = _host.Services.GetRequiredService<MainWindow>();
        mainWindow.Show();
    }

    protected override async void OnExit(ExitEventArgs e)
    {
        if (_host is not null)
        {
            await _host.StopAsync();
            _host.Dispose();
        }

        base.OnExit(e);
    }
}

这种写法的要点有三个。

  1. 在显示 UI 之前先启动 host
  2. 退出时显式地 await StopAsync
  3. 把 DI / hosted service / shutdown timeout 统一放在入口处

OnExit 改成 async 本身,出于 UI 框架的原因需要稍加留意, 但明确写出「退出时停止 host」这一流程的意义非常大。

5.2. BackgroundService

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

namespace DesktopHostSample;

public sealed class DevicePollingBackgroundService(
    IServiceScopeFactory scopeFactory,
    StatusStore statusStore,
    ILogger<DevicePollingBackgroundService> logger) : BackgroundService
{
    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Device polling service is starting.");
        await base.StartAsync(cancellationToken);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Device polling loop started.");

        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                using IServiceScope scope = scopeFactory.CreateScope();
                IDeviceStatusReader reader =
                    scope.ServiceProvider.GetRequiredService<IDeviceStatusReader>();

                DeviceStatus status = await reader.ReadAsync(stoppingToken);
                statusStore.Update(status);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Device polling failed.");
            }
        }

        logger.LogInformation("Device polling loop finished.");
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Device polling service is stopping.");
        await base.StopAsync(cancellationToken);
        logger.LogInformation("Device polling service stopped.");
    }
}

这里重要的是,把 ExecuteAsync 老老实实地写成 「受管理的 while 循环」。

  • 周期用 PeriodicTimer
  • 停止用 stoppingToken
  • 异常进行日志记录
  • 如果需要 scoped 依赖,每次都单独创建 scope

保持这种形式之后, 「这个常驻处理现在从哪里开始、在哪里停止、在哪里能看到失败」 就会变得相当易读。

5.3. 状态共享不要直接对接 UI

如果 worker 直接操作 UI 对象,最终还是会在那里重新引发 UI 线程问题。

因此,首先应该:

  • worker 更新 状态存储或消息层
  • UI 在 自己的上下文 中读取 / 反映该状态

这样的分离方式更安全。

StatusStore 可以做成类似下面这样的一个轻薄共享层。

namespace DesktopHostSample;

public sealed class StatusStore
{
    private readonly object _gate = new();
    private DeviceStatus _current = DeviceStatus.Empty;

    public DeviceStatus Current
    {
        get
        {
            lock (_gate)
            {
                return _current;
            }
        }
    }

    public void Update(DeviceStatus next)
    {
        lock (_gate)
        {
            _current = next;
        }
    }
}

public sealed record DeviceStatus(string Message)
{
    public static readonly DeviceStatus Empty = new("No Data");
}

如果需要向 UI 进行即时通知,可以使用 Dispatcher / BeginInvoke / 事件 / messenger 等方式。 不过,把这部分职责放在 UI 边界一侧,会更不容易混杂在一起。

6. StartAsync / ExecuteAsync / StopAsync 的划分方式

这三者一旦混在一起,读者的思路很快就会变得混乱。 下面这种划分方式相当稳妥。

6.1. StartAsync

StartAsync 是放置参与启动的简短处理的地方。

适合放在这里的内容:

  • 启动日志
  • 轻量级的订阅启动
  • 能很快完成的初始状态准备
  • base.StartAsync 前后进行的最小限度整理

不适合放在这里的内容:

  • 需要花费几十秒的 warm-up
  • 无限循环
  • 排列着大量繁重 I/O 的主体处理

如果让 StartAsync 变得沉重,整个应用的启动看起来都会变得迟缓。 把这里当作「写下启动信号」的地方来看待,出问题的概率会更低。

6.2. ExecuteAsync

ExecuteAsync服务生命周期的主体

适合放在这里的内容:

  • 轮询
  • 监控循环
  • 重新连接循环
  • 读取 Channel<T> 的消费者
  • 周期性处理
  • 「一直存活到停止为止」的处理整体

这里的要点有三个。

  1. 从头到尾贯穿使用 CancellationToken
  2. 避免因异常导致整个循环无声无息地死掉
  3. 不要临时应付式地过度增加重试或 backoff

BackgroundService 很方便,但放任不管的话,也会变成一个「什么都往里塞的巨大循环」。 把实际处理拆分到别的服务中,让 ExecuteAsync 本身专注于生命周期管理和编排,会更易读。

6.3. StopAsync

StopAsync 是进行正常退出时收尾整理的地方。

适合放在这里的内容:

  • 停止日志
  • 解除定时器 / 订阅 / 监控
  • 想显式 close / flush 的资源整理
  • 通过 base.StopAsync 等待结束

不过,不要对 StopAsync 抱有过度的期待,这一点也很重要。

  • 进程崩溃了
  • 被强制终止了
  • 被 OS 强制 kill 了

在这类退出方式下,StopAsync 本身可能根本不会被执行。

所以,

  • 持久化尽量在平时就以小步完成
  • 不要设计成只有在退出时才能保持一致性
  • 让 cleanup 保持幂等(idempotent)

这些地方很重要。 如果只指望在退出的那一刻拯救整个世界,大多会变得一团糟。

6.4. .NET 10 以后的注意事项

作为 2025 年以后的变更点,在 .NET 10 中,BackgroundService.ExecuteAsync 的整体行为已改为作为后台任务执行。

此前存在一种不太容易察觉的行为:第一个 await 之前的同步部分会在启动时阻塞其他服务的启动。 经过这次变更,ExecuteAsync「最初几行拖慢启动」的问题变得更容易避免。

不过,即便如此,在设计上依然建议区分为

  • 参与启动的简短处理 → StartAsync
  • 长期运行的主体 → ExecuteAsync

这样会更易读。

如果想更严格地控制启动时机,还可以考虑使用 IHostedLifecycleService。 这是常驻应用逐渐变得庞大时会发挥作用的一个不太起眼却很重要的点。

7. 常见的反模式

7.1. 在 Window_Loaded / Form_Shown 中启动无限循环

一开始很轻松。 但停止职责和异常职责会牢牢地黏在 UI 一侧。

「关闭界面就停止」 「最小化到托盘时不停止」 「设置变更时要重启」 类似这样的条件一旦开始增多,很快就会变得痛苦。

7.2. 把 Task.Run 一丢了之

Task.Run 本身并不是坏事。 不好的是没有人负责管理它的生命周期和异常

尤其是用 Task.Run(async () => { while (...) { ... } }) 来启动常驻处理时,

  • 什么时候结束
  • 谁来等待它
  • 异常怎么处理
  • 退出时要等到什么程度

都会变得模糊不清。

只要把它搬到 BackgroundService 上,就会变得相当容易整理。

7.3. 从 BackgroundService 直接操作 UI

这是个地雷。 UI 线程问题和 lifetime 问题会一下子混在一起。

worker 不要直接操作 UI,而是通过

  • 状态
  • 事件
  • 消息
  • queue

中的某一种方式设置边界,这样更安全。

7.4. 只把重要的保存处理寄托给 StopAsync

StopAsync 有助于正常退出,但它并不是最后的审判。

只在退出时才保存、 只在退出时才 flush、 只在退出时才能保持一致性,

这样的设计一旦遇到崩溃就会垮掉。

7.5. 明明用了 host,却用 Environment.Exit 草草收场

这种情况也很常见。

出于「太麻烦了,直接退出算了」的想法而调用 Environment.Exit, 就会亲手切断 host 原本具备的 graceful shutdown 路径。

如果因为致命错误而想让整个应用退出, 首先应该使用 IHostApplicationLifetime.StopApplication(), 走正规的停止路径,这样才是更自然的做法。

8. 代码评审时的检查清单

在评审使用 Generic Host / BackgroundService 的桌面应用时,按下面的顺序检查会比较清晰。

  • 该处理是挂在应用生命周期上的处理,还是单纯的 UI 事件处理
  • 启动职责是否恰当地划分到了 StartAsync / ExecuteAsync / StopAsync
  • StartAsync 是否变得过于沉重
  • ExecuteAsync 是否将 CancellationToken 一直传递到最后
  • 是否有 hosted service 直接持有 scoped 依赖
  • worker 是否直接操作了 UI 对象
  • 异常是否被无声吞掉
  • 重试循环是否变成了无限高频
  • 退出时的等待时间是否有上限
  • 是否混入了依赖 Environment.Exit 或进程 kill 的退出方式

用这份检查清单来看, 「先随便引入了 Host」 和 「把生命周期作为设计整理清楚了」 之间的差距会变得相当明显。

9. 简要的使用区分

想做的事 首选方案
统一整个应用的 DI / 日志 / 配置 Host.CreateApplicationBuilder
运行常驻循环 BackgroundService
以固定间隔运行 PeriodicTimer + BackgroundService
按顺序执行后处理 Channel<T> + BackgroundService
使用 scoped service IServiceScopeFactory.CreateScope()
向整体通知正常退出 IHostApplicationLifetime.StopApplication()
UI 更新 在 UI 侧使用 Dispatcher / Invoke
仅执行一次的界面操作 普通的 async 方法
启动时的严格生命周期控制 考虑使用 IHostedLifecycleService

10. 总结

把 Generic Host / BackgroundService 引入桌面应用的理由, 并不是「想采用类似 Web 的写法」。

真正起作用的是以下三点。

  1. 能把启动与停止的职责集中到一处
  2. 能把长期存活处理的生命周期作为设计来持有
  3. 能从入口就处理 graceful shutdown,而不是事后补救

Windows 工具或常驻类应用,即便一开始规模很小, 监控、同步、重新连接、队列、日志、配置也会逐渐增多。 到那时,如果只是把它们当作 UI 代码的附属品来运营,后面就会不知不觉地变得痛苦。

反过来,只要做到

  • UI 归 UI
  • 常驻处理归 hosted service
  • 实际处理归 DI 服务
  • 退出交给 StopAsyncCancellationToken

这样划分,情况就会相当井井有条。

这并不华丽。 但这类不起眼的设计在实际工作中确实很管用。 它能减少「关闭时偶尔会出问题」 「不知道在哪里停止」 之类令人不快的黏滞感。

如果您在 Windows 工具或常驻类应用中,遇到 BackgroundService 化、启动 / 停止设计、监控循环、COM / socket / 文件监控的生命周期整理、退出时故障排查等方面的问题,欢迎从设计评审或方针整理阶段开始咨询。

11. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

Generic Host 在桌面应用中也能使用吗?
可以使用。Generic Host 不仅是 ASP.NET Core 专属的机制,在控制台、worker、WPF / WinForms 桌面应用中,也可以作为统一负责启动、依赖关系、配置、日志、停止的基础设施来使用。它尤其适合常驻应用、托盘应用、设备监控、定期同步、有序后处理、重新连接循环等场景。
BackgroundService 是用来做什么的?
它是一个容器,用于把「长期存活的处理」纳入受管理的生命周期,而不是随手丢给 Task.Run。它是 IHostedService 的一种便于编写的实现辅助,可以把监控循环或定期处理的主体写在 ExecuteAsync 中。这样一来,「这个处理会在应用运行期间一直存活」的声明就以代码的形式体现出来,启动职责、停止职责、异常监控、日志、DI、配置都能集中到同一处设计中。
StartAsync / ExecuteAsync / StopAsync 应该如何划分?
把参与启动的简短初始化放进 StartAsync,长期运行的主体放进 ExecuteAsync,退出时的停止通知或 flush、close 放进 StopAsync,这样划分会更易读。另一方面,只在点击按钮时执行一次的处理,用普通的 async 方法就足够了,把所有处理都做成 BackgroundService 会显得有点大动干戈。
可以把退出处理全部交给 StopAsync 吗?
不太好。StopAsync 虽然方便,但它并不是进程崩溃或强制终止时的保险,所以不要把收尾工作过度依赖于它,这一点很重要。graceful shutdown(取消正在进行的 I/O、停止下一个周期、决定队列剩余任务的处理方针、关闭连接、flush 日志)应该通过 CancellationToken 和 StopAsync 来设计,同时还需要单独保证在异常退出时也不会崩坏的前提。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表