如何在Ubuntu的systemd上开发.NET Core 3.1常驻控制台应用及最佳实践?
在Ubuntu systemd环境下开发.NET Core 3.1常驻服务的最佳实践
咱们先拆解下你遇到的两个问题:
- 用
Console.ReadKey()报错:因为systemd运行的服务没有交互式控制台,输入输出被重定向了,Console.ReadKey()依赖控制台环境,自然会抛出InvalidOperationException。 - 换成
while(true){}导致CPU占用100%:这是空循环,线程一直在无意义地跑,直接占满CPU核心,使用率拉满是必然的。
下面给你几种靠谱的解决方案,按推荐优先级排序:
1. 官方推荐:使用Generic Host框架(最佳实践)
.NET Core 3.1已经提供了专门用于后台服务的Generic Host,它自带生命周期管理、依赖注入、配置、日志等能力,完美适配systemd服务场景,是生产环境的首选方案。
步骤:
(1)引入依赖
如果是现有控制台项目,先安装NuGet包:
dotnet add package Microsoft.Extensions.Hosting
(2)编写后台服务类
创建一个继承自BackgroundService的类,实现核心业务逻辑:
using Microsoft.Extensions.Hosting; using System; using System.Threading; using System.Threading.Tasks; public class MyBackgroundService : BackgroundService { private readonly ILogger<MyBackgroundService> _logger; private System.Timers.Timer _timer; public MyBackgroundService(ILogger<MyBackgroundService> logger) { _logger = logger; } // 服务启动时执行初始化 public override Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("后台服务启动"); // 初始化你的定时器任务(示例:每5秒执行一次) _timer = new System.Timers.Timer(5000); _timer.Elapsed += (sender, e) => { _logger.LogInformation("定时器任务执行中..."); // 这里写你的业务逻辑 }; _timer.Start(); return base.StartAsync(cancellationToken); } // 服务停止时执行清理 public override Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("后台服务停止"); _timer?.Stop(); _timer?.Dispose(); return base.StopAsync(cancellationToken); } // 后台服务的主循环(这里依赖定时器,无需额外循环) protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 如果不用定时器,也可以在这里写带延迟的循环: // while (!stoppingToken.IsCancellationRequested) // { // _logger.LogInformation("循环任务执行中..."); // await Task.Delay(5000, stoppingToken); // } // 保持服务运行,直到收到停止信号 await Task.Delay(Timeout.Infinite, stoppingToken); } }
(3)修改Main方法
用HostBuilder构建并运行服务:
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; class Program { static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddHostedService<MyBackgroundService>(); }); }
(4)配置systemd服务
创建一个服务文件,比如my-dotnet-service.service,放在/etc/systemd/system/目录下:
[Unit] Description=我的.NET Core后台服务 After=network.target [Service] # 替换成你的应用dll路径 ExecStart=/usr/bin/dotnet /opt/myapp/MyApp.dll # 运行服务的用户,建议用非root用户 User=www-data # 服务崩溃后自动重启 Restart=always # 重启间隔5秒 RestartSec=5 # 环境变量,比如指定.NET运行环境 Environment=ASPNETCORE_ENVIRONMENT=Production [Install] WantedBy=multi-user.target
然后执行以下命令启用并启动服务:
# 重新加载systemd配置 sudo systemctl daemon-reload # 设置开机自启并启动服务 sudo systemctl enable --now my-dotnet-service.service # 查看服务状态 sudo systemctl status my-dotnet-service.service
2. 轻量方案:使用ManualResetEvent(无需Host框架)
如果你不想引入Host框架,可以用ManualResetEvent让主线程阻塞,避免空循环占用CPU,同时支持优雅停止。
示例代码:
using System; using System.Threading; class Program { private static ManualResetEvent _quitEvent = new ManualResetEvent(false); private static System.Timers.Timer _timer; static void Main(string[] args) { // 捕获systemd的停止信号(SIGTERM) AppDomain.CurrentDomain.ProcessExit += (sender, e) => { _quitEvent.Set(); _timer?.Stop(); _timer?.Dispose(); Console.WriteLine("服务已停止"); }; // 初始化定时器任务 _timer = new System.Timers.Timer(5000); _timer.Elapsed += (sender, e) => { Console.WriteLine("定时器任务执行中..."); // 业务逻辑 }; _timer.Start(); Console.WriteLine("服务启动成功,等待停止信号..."); // 阻塞主线程,直到收到停止信号 _quitEvent.WaitOne(); } }
这个方案轻量简洁,但没有Host框架的生命周期管理、依赖注入等能力,适合简单的小服务场景。
3. 优化空循环:加入Task.Delay
如果你坚持用循环实现,一定要加入延迟让线程休眠,避免CPU占用过高:
using System; using System.Threading; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { var cancellationTokenSource = new CancellationTokenSource(); AppDomain.CurrentDomain.ProcessExit += (sender, e) => { cancellationTokenSource.Cancel(); Console.WriteLine("服务已停止"); }; // 初始化定时器 var timer = new System.Timers.Timer(5000); timer.Elapsed += (sender, e) => { Console.WriteLine("定时器任务执行中..."); }; timer.Start(); try { // 带延迟的循环,不会占用CPU while (!cancellationTokenSource.Token.IsCancellationRequested) { await Task.Delay(1000, cancellationTokenSource.Token); } } catch (OperationCanceledException) { // 捕获取消信号,做清理工作 timer.Stop(); timer.Dispose(); } } }
这里用Task.Delay让线程每次循环休眠1秒,CPU使用率会降到几乎为0,同时能响应停止信号实现优雅退出。
最后总结
生产环境优先推荐Generic Host框架,它能帮你处理很多服务生命周期的细节,比如优雅启动/停止、日志、配置等,是.NET Core后台服务的标准最佳实践。
内容的提问来源于stack exchange,提问作者hooboe
相关产品推荐
相关产品推荐

