Windows 服务的创建与运维 ── 从任务计划程序的取舍到 BackgroundService 服务化

· · Windows 服务, Windows, .NET, C#, BackgroundService, Generic Host, 常驻处理, 运维, 判断表, 技术咨询

「每 5 分钟跑一次的任务计划程序已经跟不上了」「想让常驻监听接收设备数据的监控常驻程序一直开着」「文件一放进文件夹就希望在几秒内处理完」。持续接触定期执行相关的咨询,需求发展到一定程度后,最后几乎都会走到这个话题。

本博客已经连续写了用 Power Automate 做业务自动化任务计划程序的安全运维设计这两篇自动化相关文章。在任务计划程序那篇的第 8 章提到「一旦需要分钟级轮询或常驻监控,就是常驻进程的领域」这样的分界,但实际上该怎么创建 Windows 服务、又该怎么让它投入运维?如果在这里含糊地「先做成服务再说」,就会生出「停不下来的服务」「崩溃了也没人发现的服务」「用 LocalSystem 跑任何东西的服务」。

本文将按实务上决策的顺序,整理常驻处理是否该做成 Windows 服务的判断表、服务机制的最低限知识(SCM・启动类型・Session 0)、用 .NET 8 的 Worker Service 进行实现、用 sc.exe 注册与设置恢复选项、运行账户的选择,以及安全的停止处理。

1. 先说结论

  • 取舍的判断轴很简单。间隔数分钟以上的定期处理,任务计划程序就够用;一旦需求里出现常驻监听(socket、命名管道、FileSystemWatcher)、秒级响应、崩溃后自动恢复,就该用 Windows 服务
  • 从 Windows Vista 开始,服务会在 Session 0 中运行,无法直接与用户交互(无法显示 UI)。如果需要 UI,就要把服务和 UI 应用程序拆开,靠进程间通信来对话。1
  • .NET 的官方创建方式,是 Worker Service 模板加上 Microsoft.Extensions.Hosting.WindowsServicesAddWindowsService(如果是 IHostBuilder 系列,则是 UseWindowsService)。可以直接当作控制台应用程序运行,让开发、调试比传统的服务开发轻松许多。2
  • .NET 6 之后,BackgroundService 内未处理的异常,默认会让主机停止BackgroundServiceExceptionBehavior.StopHost)。但这属于「正常停止」,所以 SCM 恢复选项带来的重启不会生效。想要重启的失败,要用非 0 的退出代码让进程终止。32
  • 别图方便就把运行账户设成 LocalSystem。sc.exe create 的默认值就是 LocalSystem,不留意的话就会以最强权限开始运行。4 只在本机完成的处理,优先考虑 LocalService 或虚拟账户;需要在域中访问共享资源或数据库的,gMSA 才是正解。5
  • 恢复选项(sc.exe failure)和停止处理(HostOptions.ShutdownTimeout,默认 30 秒6)要在注册时就设计好。「对停止没有响应的服务」是运维现场最不受欢迎的存在。

2. 什么时候该做成服务 ── 任务计划程序・服务・常驻应用程序的判断表

出现类似常驻需求时,可选的方案有 3 种:任务计划程序、Windows 服务,以及「注册到启动项的常驻应用程序」(常驻在通知区域那种)。先把各自的特性列出来。

角度 任务计划程序 Windows 服务 常驻应用程序(启动项)
启动时机 依时间・事件触发每次启动 OS 启动时(登录前)就常驻 用户登录时开始
是否需要登录 可以不需要(非交互执行) 不需要 需要(注销就消失)
UI 无法显示(非交互配置时) 无法显示(Session 0) 可以显示(通知区域・对话框)
常驻或定期 定期(适合以小时到每天为单位) 常驻 常驻(但局限在用户会话内)
权限 每个任务各自指定运行账户 服务账户(第 6 章) 登录用户的权限
监控・恢复 历史记录+自行通知 SCM 的恢复选项(自动重启) 没有(要自己做)
部署的工作量 用 XML/PowerShell 注册 sc.exe/安装程序 注册到 Run 注册表项等

判断的思路如下。

  • 每天数次到以小时为单位的定期处理,且处理之间不需要保留状态,用任务计划程序就好。特意把它做成服务、自己管理定时器是过度设计,任务计划程序那篇文章里写的运维设计成本要低得多。
  • 本质上是常驻监听──TCP 或管道的请求等待、FileSystemWatcher 文件夹监控、队列的逐一处理、秒级响应──就用 Windows 服务。如果开始用「每 5 分钟一个任务」把处理切碎去轮询,那就是在劣化重造常驻进程的信号。
  • UI 才是主体(通知区域操作、向用户弹窗)就用常驻应用程序。但它必须登录才会运行,所以不适合服务器类用途。如果「后台处理需要常驻运行,但也需要 UI」,就按下一章所说,拆成服务和 UI 应用程序两个进程。

还有一个容易被忽略的判断依据是恢复。任务计划程序的任务即使失败,也只能放着等到下次调度;服务则有 SCM 帮忙处理自动重启(第 5 章)。「就算夜里崩溃了,也希望能在早上之前自行恢复」这种需求,本身就足以成为做成服务的理由。

3. 服务机制的最低限知识 ── SCM・启动类型・Session 0

3.1 SCM 与启动类型

Windows 服务背后的操盘手是服务控制管理器(SCM)。它管理服务的登记簿,中介启动、停止的请求,并在失败时执行恢复动作。服务进程必须按照约定与 SCM 对话,但这部分已经由 .NET 的库代劳(第 4 章),我们在设计上要决定的,只有「启动类型」「运行账户」「恢复」「停止」这 4 点。

启动类型(Startup type)有 4 种可选。4

启动类型 行为 适用场景
自动(auto) OS 启动时开始 常驻运行服务的基本选项
自动(延迟启动,delayed-auto) 在其他自动服务之后稍晚开始 业务服务优先选这个,避免启动刚完成时的拥堵与依赖对象尚未就位
手动(demand) 只在被请求时才开始 由其他应用程序唤起的辅助服务
已禁用(disabled) 无法启动 用于停用措施、封存

业务服务应该以延迟启动为默认值。OS 刚启动时,网络、数据库、其他服务都还没到位,如果用「自动」抢最快启动,很容易在第一次连接时就失败。用延迟启动拉开时间差,再靠后面提到的 ExecuteAsync 内部重试机制吸收连接失败,这种双重保障会更稳定。

另外还要知道启动超时这件事。SCM 不会无限期等待服务汇报启动完成。超过默认超时(ServicesPipeTimeout,30 秒)就会记录事件 7000/7011,并视为启动失败。7 也就是说,像数据库连接重试、构建大型缓存这类繁重的初始化,不该放在「启动处理」里执行。原则是让启动立刻完成,繁重的处理留给主体循环(ExecuteAsync)去做。

3.2 Session 0 隔离 ── 服务无法显示 UI

从 Windows Vista 开始,服务会在名为 Session 0 的隔离会话中运行,无法直接与用户交互1 就算在服务里调用 MessageBox.Show 或显示表单,登录中用户的屏幕上也不会出现任何东西。更糟的是,它会一直等待没有人能点击的「确定」按钮,让整个处理卡住,这是经典的假死成因。把旧代码搬进服务时,一定要确认是否还残留着原本用来显示错误的消息框。

需要 UI 时的正解,是把服务(Session 0)和 UI 应用程序(用户会话)拆成两个进程,靠进程间通信(IPC)对话。这也是 Microsoft 官方建议的设计,用命名管道之类的 IPC,让 UI 端把结果回传给服务。1 该选哪种 IPC 手段,整理在同日发布的姊妹文章「进程间通信的判断表」里。把 UI 应用程序端也放到 Generic Host 上,就能让两个进程在 DI、日志、配置的架构上保持一致(参见「在桌面应用程序中使用 Generic Host 与 BackgroundService」)。

4. .NET 中的创建方式 ── Worker Service 与 AddWindowsService

4.1 模板与 Program.cs

.NET 的服务开发,要从 Worker Service 模板开始。它已内置在 SDK 中,用 dotnet new worker 就能生成雏形,接着只要添加 Microsoft.Extensions.Hosting.WindowsServices 包并调用 AddWindowsService,就准备好可以当 Windows 服务运行了。2 作为基础的 Generic Host 机制,在「什么是 .NET Generic Host」里有说明。

using App.MonitorService;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// 添加以 Windows 服务启动时与 SCM 对话用的生命周期。
// 若以控制台方式启动,则维持原本的 ConsoleLifetime 运行
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "KsMonitor";
});

builder.Services.AddSingleton<MeasurementQueue>();
builder.Services.AddHostedService<MonitorWorker>();

IHost host = builder.Build();
host.Run();

如果是使用 Host.CreateDefaultBuilder 的传统风格项目,就改成调用 IHostBuilder 扩展方法 UseWindowsService(),作用相同。

有一个和任务计划程序文章里「开始(可选)」问题同类型的陷阱。以服务方式启动时,当前工作目录是 C:\Windows\System32。如果 appsettings.json 之类的文件是以相对路径为前提去读取,就会出现「控制台能跑,服务却读不到配置」的状况。请让配置文件以可执行文件所在位置为基准来解析,或依官方教程的指示,在注册时的 binpath 中加上 --contentRoot,明确指定内容根目录。2

4.2 ExecuteAsync ── stoppingToken 与异常设计

主体要写在继承自 BackgroundServiceExecuteAsync 里。这里该决定的,只有「如何响应停止请求」和「如何处理异常」这两件事。

namespace App.MonitorService;

public sealed class MonitorWorker(
    MeasurementQueue queue,
    ILogger<MonitorWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    // 从队列取出一条进行处理。等待时务必一律传入 token
                    var item = await queue.DequeueAsync(stoppingToken);
                    await ProcessAsync(item, stoppingToken);
                }
                catch (Exception ex) when (ex is IOException or TimeoutException)
                {
                    // 可以继续运行的失败:记录后间隔一段时间再重试
                    logger.LogError(ex, "处理失败,将于 30 秒后重试。");
                    await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
                }
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // 来自 SCM 的停止请求,属于正常流程,不做任何事。
            // 用 when 子句限定为 stoppingToken 触发的原因,是为了避免内部处理
            // 超时等状况抛出的取消被误判成「正常停止」,导致工作线程
            // 悄悄停下、而主机却继续存活
        }
        catch (Exception ex)
        {
            // 未预期的失败:默认的 StopHost 会让主机「正常」停止,
            // 导致 SCM 的恢复选项(自动重启)不会触发。
            // 用非 0 的退出代码让进程终止,告诉 SCM「这是失败」
            logger.LogCritical(ex, "发生无法恢复的错误,服务即将终止。");
            Environment.Exit(1);
        }
    }

    private Task ProcessAsync(Measurement item, CancellationToken token)
        => throw new NotImplementedException();
}
  • stoppingToken 传给所有等待。 Task.Delay、I/O、队列等待都要传。只要有一处忽略了这个 token 的长时间等待,那里就会变成「对停止没有响应的服务」的温床(第 7 章)。
  • 把可以继续运行的失败和无法恢复的失败分开。 网络中断、临时性超时之类的状况,要在循环内捕获、记录并重试。只是吞掉异常再 continuecatch (Exception),只会做出一个悄悄空转的服务。该怎么分类的思路,整理在「遇到未预期异常时该终止还是继续的判断表」里。
  • 要知道未处理异常的默认行为。 .NET 6 之前,从 ExecuteAsync 泄漏出来的异常会凭空消失,服务看起来还在运行,实际上却什么都不做,变成「僵尸」。.NET 6 之后,默认行为变成 BackgroundServiceExceptionBehavior.StopHost,异常会先记录到日志,再让主机停止。3 但这个停止属于「正常停止」,就算设置了恢复选项也不会重启。想靠恢复选项恢复的失败,就要像上面的代码那样,用 Environment.Exit(1) 明确地让它异常终止,这才是官方教程所示范的做法。2

4.3 可以直接以控制台运行 ── 开发变轻松的最大原因

AddWindowsService 只会在以 Windows 服务方式启动时,才切换成服务专用的生命周期。也就是说,同一个 exe 在 Visual Studio 按 F5 或用 dotnet run 时,会当作普通的控制台应用程序运行。断点照样能命中,按下 Ctrl+C 就能在本机验证从停止请求到 StopAsync 的整个流程。

不过,控制台上能跑,不代表服务也一定能跑。运行账户的差异(第 6 章)、Session 0、当前工作目录——只有这 3 点,一定要以服务身份在实机上验证。「控制台能跑、服务却不能跑」的原因,几乎都会收敛到这 3 点上。

5. 注册与运维 ── sc.exe・恢复选项・事件日志

5.1 用 sc.exe create 注册

把发布(dotnet publish,发布成单文件会更好处理)出来的 exe,用 sc.exe create 注册到 SCM。要在管理员权限的 PowerShell 中执行。

sc.exe create "KsMonitor" binpath= "C:\Services\KsMonitor\KsMonitor.exe" start= delayed-auto obj= "NT AUTHORITY\LocalService" displayname= "KS 测量数据导入服务"
sc.exe description "KsMonitor" "将测量数据导入并写入数据库的常驻服务(负责部门:信息系统部)"

先说一个经典陷阱。binpath=start= 的等号后面必须有空格。等号之前是选项名,选项名和值之间的空格在语法上是必须的,这是 sc.exe 特有的规则(省略就会失败)。4 另外,省略 obj= 时的默认值是 LocalSystem。4 什么都不设置就直接注册,就会以最强权限开始运行,所以请在注册时就完成第 6 章提到的选择。

设置 description 看似不起眼却很重要。留下足够信息,让几年后打开 services.msc 的人能判断「这是什么服务、能不能停、该问谁」,之后的排查成本就能省下来。移除时要先停止服务,再执行 sc.exe delete "KsMonitor"2

如果部署对象有多台机器,或是需要定期更新(替换文件),就尽早改用安装程序(MSI/WiX 的 ServiceInstall 等)把注册、更新、移除整套处理好,而不是靠 sc.exe 手动操作。手动注册的操作手册,一定会产生某台漏了一步的机器。

5.2 恢复选项 ── 「挂了就重启」交给 SCM 处理

服务化的一大优点,就是 SCM 内置的恢复选项。它让你可以用声明式的方式,设置进程异常终止时该做什么。2

sc.exe failure "KsMonitor" reset= 86400 actions= restart/60000/restart/60000/restart/300000

这个示例的意思是「第 1、第 2 次失败,60 秒后重启;第 3 次及以后,5 分钟后重启;失败计数器每 24 小时重置一次」。用图形界面(services.msc 属性 →「恢复」选项卡)也能做同样的设置。在自己动手打造「监视任务」或监控脚本之前,请先用好这个标准功能。

有两点要注意。第一,如前一章所说,进程必须以非 0 退出代码终止才会触发恢复。StopHost 造成的正常停止不在恢复范围内。第二,如果是启动后必定崩溃的状态(配置错误、数据库连不上),就会变成重启循环。这也是为什么第 3 次及以后的间隔要拉长,同时要提前做好「为什么会崩溃」能留在事件日志里(下一节)的准备。

5.3 事件日志与文件日志并用

Host.CreateApplicationBuilder 在 Windows 上会自动添加 EventLog 日志提供程序。而默认只有 Warning 及以上级别的消息才会发送到事件日志。2 很容易让人误以为「做成服务之后 Information 日志全都不见了」,但其实不是消失,而是被事件日志的默认过滤器挡住了。如果也想记录启动、停止之类的运维事件,就要在 appsettings.json 中明确设置 EventLog 提供程序的级别。

{
  "Logging": {
    "EventLog": {
      "SourceName": "KsMonitor",
      "LogLevel": {
        "Default": "Warning",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

有一点要注意。事件源(Event Source)如果没有事先注册就无法写入,而注册需要管理员权限。省略 SourceName 也逃不掉——因为 AddWindowsService 默认会用应用程序名作为源名称2,所以无论如何都是从「未注册的源」开始。像 LocalService 这种最小权限账户,在运行期无法创建源,一旦创建失败,事件日志就会什么都写不进去,运维却已经开始了。注册工作请交给安装程序(或具有管理员权限的配置流程)处理。这和任务计划程序文章第 6 章写的「把 CreateEventSource 分离到配置流程那一侧」是同一件事。

区分使用的原则是:事件日志只放「运维人员该看的东西」(启动、停止、失败、恢复),详细的处理追踪则交给自己维护的文件日志。自行维护文件日志时的最低要求(滚动、写入失败时的行为),整理在「自制日志组件的最低要求」;进程整个崩溃时该如何留下证据的设计,整理在「崩溃时留存日志与转储文件的设计」。因为在靠恢复选项自动重启的架构下,崩溃那一刻的记录有没有留在现场,会是唯一的线索。

6. 运行账户 ── 别图方便就选 LocalSystem

服务会在指定账户的安全上下文下运行,SCM 在启动时会用该账户登录,并把令牌附加到进程上。8 这就是任务计划程序文章第 3 章写的「以谁的身份运行」,换成服务版的说法。先把各个选项列出来。

账户 权限 在网络上的身份 密码管理 适用场景
LocalSystem 极强(相当于 OS 层级) 计算机账户 不需要 原则上要避免。因为是 sc.exe create 的默认值,要特别注意
LocalService 最小限 匿名(无法访问共享资源) 不需要 本机内完成处理的第一候选
NetworkService 最小限 计算机账户 不需要 需要访问域内资源的轻量处理
虚拟账户(NT SERVICE\服务名) 只有被授予的部分 计算机账户 不需要 可按服务单位授予 ACL,对本机资源采用最小权限
gMSA 只有被授予的部分 gMSA 本身 由 DC 自动管理 需要在域中访问共享资源/数据库的服务首选

判断的重点有 3 个。

  • 不要因为「这样能跑」就选 LocalSystem。 服务的漏洞会直接等同于整台机器被接管。是否真的需要相当于管理员的权限,可以直接套用「什么时候才真的需要管理员权限」里的整理。多数情况下,需要的顶多是「写入特定文件夹」这种程度,只要把该文件夹的 ACL 授予虚拟账户(NT SERVICE\KsMonitor)就够了。虚拟账户不需要创建,也不需要密码管理。5
  • 网络共享的陷阱。 LocalService 在网络上会变成匿名身份,所以访问 \\server\share 会失败。NetworkService、虚拟账户、LocalSystem 都会以计算机账户(DOMAIN\机器名$)的身份出现在网络上5,所以共享那一端要对这个计算机账户同时开放共享权限和 NTFS 权限。「明明给了用户权限,但只有服务读不到」的真相,多半就是这个。先决定好「要以谁的身份出现在网络上」,再去配置共享那一端。
  • 要用普通用户账户运行,就要连密码运维一起考虑。 使用专用账户时,需要「作为服务登录」的权利,而且密码一旦过期,登录就会失败,服务也就无法启动。8 这是「密码变更让任务悄悄死掉」问题的服务版。如果是域环境,用密码由域端自动管理的 gMSA,把这整个问题一起消掉才是正解(和任务计划程序文章 3.2 节的结论一致)。

7. 安全停止 ── ShutdownTimeout 与在途处理

先掌握停止的流程。当 services.msc、sc.exe stop,或是 OS 关机让 SCM 发出停止请求时,.NET 的服务生命周期会把它转换成主机停止(StopApplication),stoppingToken 会被取消,ExecuteAsync 退出,接着调用各服务的 StopAsync。主机等待这一连串停止处理的时间,就是 HostOptions.ShutdownTimeout,默认是 30 秒。69 如果来不及完成,就会在后续收尾工作尚未完成的情况下强行停止。

如果 30 秒不足以写完一条在途处理,就从所需时间反推、延长这个超时。

builder.Services.Configure<HostOptions>(options =>
{
    // 从写完一条在途处理所需的最长时间反推
    options.ShutdownTimeout = TimeSpan.FromSeconds(90);
});

在此基础上,停止处理的设计要注意 3 点。

  • 先决定好收到停止请求后的执行顺序。 停止接受新任务 → 让在途处理完成(或在安全的断点处中断,并以能够续跑的方式记录下来) → 清理连接和临时文件。没有决定这个顺序,就会走向两个极端:一看到 token 就把所有东西全丢掉,或是想全部做完而导致超时。
  • 不要做出「对停止没有响应的服务」。 只要有一处忽略 token 的长循环或长时间等待,就会变成在 SCM 上卡在「正在停止」的服务。这会妨碍 Windows Update 的重启,并在每次服务器计划性停机时都需要手动 kill,是运维现场最不受欢迎的缺陷。如第 4 章所说,请在每个处理节点检查 token,并把 CancellationToken 传给长时间的 I/O。用控制台方式运行的话,按 Ctrl+C 就能原样测试这个停止流程,建议把「按下 Ctrl+C 后几秒内结束」纳入发布前的测试项。
  • 把「即使被 kill 也不会坏」的设计,看得比停止处理做得多细致更重要。 断电或强制终止,不管停止处理做得多完善都还是会发生。事务、原子性的文件写入、可重跑的处理单元——「就算中途死掉,下次启动时也能保持一致」才是根本,做得细致的停止处理,终究只是礼貌问题。

8. 总结

判断很简单:定期处理用任务计划程序;需要常驻监听、秒级响应、自动恢复就用 Windows 服务;UI 才是主体就用常驻应用程序。一旦决定做成服务,用 .NET 的 Worker Service 和 AddWindowsService,采取「以控制台开发、以服务部署」的形式,就是目前的标准做法。

在注册时需要作为设计决定的有 4 点——启动类型(业务服务用延迟启动)、运行账户(避开 LocalSystem,改用虚拟账户或 gMSA)、恢复选项(sc.exe failure 搭配 Environment.Exit(1))、停止(ShutdownTimeout 与在途处理的收尾)。只要掌握这 4 点,加上尊重 stoppingTokenExecuteAsync 写法,Windows 服务就不再是「特别难做的东西」。反过来说,没有决定这 4 点就开始运行的服务,几年后往往会带着「停不掉、崩溃了也没人发现、权限太强不敢动」三重苦回来找你。要重整现有服务,从盘点这 4 点开始,也是最快的捷径。

相关文章

相关咨询领域

合同会社小村软件提供常驻处理的设计评审(包括从任务计划程序迁移的判断),以及「对停止没有响应」「不知不觉就崩溃」的 Windows 服务排查与重构相关咨询。

参考链接

  1. Microsoft Learn,Interactive Services。说明 Windows Vista 之后服务无法直接与用户交互、服务会在 Session 0 中运行,以及需要 UI 时建议采用另一个 GUI 应用程序进程搭配 IPC 的设计。  2 3

  2. Microsoft Learn,Create Windows Service using BackgroundService。把 Worker Service 做成 Windows 服务的官方教程,说明 AddWindowsService、EventLog 提供程序默认级别为 Warning、用 sc.exe 进行注册/恢复/删除,以及恢复选项需要非 0 退出代码。  2 3 4 5 6 7 8 9

  3. Microsoft Learn,.NET 6 breaking change: Exception handling in hosting。说明 .NET 6 之后,BackgroundService.ExecuteAsync 的未处理异常会先记录到日志,再依默认设置让主机停止(BackgroundServiceExceptionBehavior.StopHost)。  2

  4. Microsoft Learn,sc.exe create。说明 start=(auto/demand/delayed-auto 等)与 obj=(默认为 LocalSystem)的规格,以及选项的等号后面必须有空格(省略会失败)。  2 3 4

  5. Microsoft Learn,Service Accounts in Windows Server。说明虚拟账户(NT SERVICE\服务名)不需要密码管理,会用计算机账户的凭据访问网络,以及 gMSA 的密码由域端自动管理。  2 3

  6. Microsoft Learn,.NET Generic Host。说明主机关闭处理的流程,以及停止时会等待可配置的关闭超时(默认 30 秒)。  2

  7. Microsoft Learn,A service does not start, and events 7000 and 7011 are logged。说明 SCM 只会等待 ServicesPipeTimeout 指定的时间让服务启动,超过就会记录事件 7000/7011,以及调高默认超时的步骤。 

  8. Microsoft Learn,Service User Accounts。说明服务会在指定账户的安全上下文下运行,SCM 在启动时会进行登录,密码过期会导致服务无法启动,以及 LocalService/NetworkService/LocalSystem 这几个特殊账户。  2

  9. Microsoft Learn,HostOptions.ShutdownTimeout Property。说明用来控制 StopAsync 默认超时的属性定义。 

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

常见问题

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

任务计划程序和 Windows 服务该怎么取舍?
如果是间隔数分钟以上的定期处理,且处理之间不需要保留状态,用任务计划程序就够了。一旦需求里出现 TCP 或管道的常驻监听、FileSystemWatcher 文件夹监控、秒级响应、崩溃后自动恢复,就该用 Windows 服务。如果开始用「每 5 分钟一个任务」把处理切碎去轮询,这就是在劣化重造常驻进程的信号。如果 UI 才是主体(比如通过通知区域操作),那么只在登录期间运行的常驻应用程序,就是第三个选项。
Windows 服务可以显示 UI(界面)吗?
不可以。从 Windows Vista 开始,服务会在称为 Session 0 的隔离会话中运行,无法直接与用户交互。就算在服务里调用 MessageBox.Show,用户的屏幕上也不会出现任何东西,反而会因为一直等待没有人能点击的「确定」按钮而让处理卡住,造成假死。如果需要 UI,正确做法是把服务和 UI 应用程序拆成两个进程,通过命名管道之类的进程间通信来对话。把旧代码搬到服务里时,一定要检查是否还残留着用来显示错误的消息框。
.NET 要怎么创建 Windows 服务?
官方做法是先从 Worker Service 模板(dotnet new worker)开始,添加 Microsoft.Extensions.Hosting.WindowsServices 包后调用 AddWindowsService。同一个 exe 在 Visual Studio 按 F5 或用 dotnet run 运行时,会当作普通的控制台应用程序运行,开发、调试因此轻松许多。注册要用 sc.exe create,但要注意它独特的规则:binpath= 或 start= 的等号后面必须有空格,而且省略 obj= 时默认会是 LocalSystem(最强权限)。服务启动时的当前工作目录是 C:\Windows\System32,所以配置文件要以可执行文件所在位置为基准来解析。
BackgroundService 发生异常时,服务会怎么样?
.NET 6 之后,从 ExecuteAsync 泄漏出来的未处理异常,默认行为(BackgroundServiceExceptionBehavior.StopHost)是先记录到日志,再让主机停止。但这会被视为「正常停止」,所以不会触发 SCM 的恢复选项(自动重启)。如果希望恢复选项能重启服务,就必须像 Environment.Exit(1) 那样,用非 0 的退出代码让进程明确地异常终止。像网络中断这类可以继续运行的失败,应该在循环内捕获、记录并重试,并在设计上与无法恢复的失败区分开来。

作者简介

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

Go Komura

小村软件有限公司 代表

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

返回博客列表