You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 07:41:24