.NET Worker Service 如何优雅退出
上一篇文章中我们了解了 .NET Worker Service 的入门知识[1],今天我们接着介绍一下如何优雅地关闭和退出 Worker Service。
Worker 类
从上一篇文章中,我们已经知道了 Worker Service 模板为我们提供三个开箱即用的核心文件,其中 Worker 类是继承自抽象基类 BackgroundService 的,而 BackgroundService 实现了 IHostedService 接口。最终 Worker 类会被注册为托管服务,我们处理任务的核心代码就是写在 Worker 类中的。所以,我们需要重点了解一下 Worker 及其基类。
先来看看它的基类 BackgroundService :

基类 BackgroundService 中有三个可重写的方法,可以让我们绑定到应用程序的生命周期中:
- 抽象方法
ExecuteAsync:作为应用程序主要入口点的方法。如果此方法退出,则应用程序将关闭。我们必须在 Worker 中实现它。 - 虚方法
StartAsync:在应用程序启动时调用。如果需要,可以重写此方法,它可用于在服务启动时一次性地设置资源;当然,也可以忽略它。 - 虚方法
StopAsync:在应用程序关闭时调用。如果需要,可以重写此方法,在关闭时释放资源和销毁对象;当然,也可以忽略它。
默认情况下 Worker 只重写必要的抽象方法 ExecuteAsync。
新建一个 Worker Service 项目
我们来新建一个 Worker Service,使用 Task.Delay 来模拟关闭前必须完成的一些操作,看看是否可以通过简单地在 ExecuteAsync 中 Delay 来模拟实现优雅关闭。
需要用到的开发工具:
- Visual Studio Code:https://code.visualstudio.com/
- 最新的 .NET SDK:https://dotnet.microsoft.com/download
安装好以上工具后,在终端中运行以下命令,创建一个 Worker Service 项目:
dotnet new Worker -n "MyService"
创建好 Worker Service 后,在 Visual Studio Code 中打开应用程序,然后构建并运行一下,以确保一切正常:
dotnet build
dotnet run
按 CTRL+C 键关闭服务,服务会立即退出,默认情况下 Worker Service 的关闭就是这么直接!在很多场景(比如内存中的队列)中,这不是我们想要的结果,有时我们不得不在服务关闭前完成一些必要的资源回收或事务处理。
我们看一下 Worker 类的代码,会看到它只重写了基类 BackgroundService 中的抽象方法 ExecuteAsync:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
我们尝试修改一下此方法,退出前做一些业务处理:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
// await Task.Delay(1000, stoppingToken);
await Task.Delay(1000);
}
_logger.LogInformation("等待退出 {time}", DateTimeOffset.Now);
Task.Delay(60_000).Wait(); //模拟退出前需要完成的工作
_logger.LogInformation("退出 {time}", DateTimeOffset.Now);
}
然后测试一下,看它是不是会像我们预期的那样先等待 60 秒再关闭。
dotnet build
dotnet run
按 CTRL+C 键关闭服务,我们会发现,它在输出 “等待退出” 后,并没有等待 60 秒并输出 “退出” 之后再关闭,而是很快便退出了。这就像我们熟悉的控制台应用程序,默认情况下,在我们点了右上角的关闭按钮或者按下 CTRL+C 键时,会直接关闭一样。
Worker Service 优雅退出
那么,怎么才能实现优雅退出呢?
方法其实很简单,那就是将 IHostApplicationLifetime 注入到我们的服务中,然后在应用程序停止时手动调用 IHostApplicationLifetime 的 StopApplication 方法来关闭应用程序。
修改 Worker 的构造函数,注入 IHostApplicationLifetime:
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<Worker> _logger;
public Worker(IHostApplicationLifetime hostApplicationLifetime, ILogger<Worker> logger)
{
_hostApplicationLifetime = hostApplicationLifetime;
_logger = logger;
}
然后在 ExecuteAsync 中,处理完退出前必须完成的业务逻辑后,手动调用 IHostApplicationLifetime 的 StopApplication 方法,下面是丰富过的 ExecuteAsync 代码:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
// 这里实现实际的业务逻辑
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await SomeMethodThatDoesTheWork(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Global exception occurred. Will resume in a moment.");
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
finally
{
_logger.LogWarning("Exiting application...");
GetOffWork(stoppingToken); //关闭前需要完成的工作
_hostApplicationLifetime.StopApplication(); //手动调用 StopApplication
}
}
private async Task SomeMethodThatDoesTheWork(CancellationToken cancellationToken)
{
_logger.LogInformation("我爱工作,埋头苦干ing……");
await Task.CompletedTask;
}
/// <summary>
/// 关闭前需要完成的工作
/// </summary>
private void GetOffWork(CancellationToken cancellationToken)
{
_logger.LogInformation("啊,糟糕,有一个紧急 bug 需要下班前完成!!!");
_logger.LogInformation("啊啊啊,我爱加班,我要再干 20 秒,Wait 1 ");
Task.Delay(TimeSpan.FromSeconds(20)).Wait();
_logger.LogInformation("啊啊啊啊啊啊,我爱加班,我要再干 1 分钟,Wait 2 ");
Task.Delay(TimeSpan.FromMinutes(1)).Wait();
_logger.LogInformation("啊哈哈哈哈哈,终于好了,下班走人!");
}
此时,再次 dotnet run 运行服务,然后按 CTRL+C 键关闭服务,您会发现关闭前需要完成的工作 GetOffWork 运行完成后才会退出服务了。
至此,我们已经实现了 Worker Service 的优雅退出。
StartAsync 和 StopAsync
为了更进一步了解 Worker Service,我们再来丰富一下我们的代码,重写基类 BackgroundService 的 StartAsync 和 StopAsync 方法:
public class Worker : BackgroundService
{
private bool _isStopping = false; //是否正在停止工作
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<Worker> _logger;
public Worker(IHostApplicationLifetime hostApplicationLifetime, ILogger<Worker> logger)
{
_hostApplicationLifetime = hostApplicationLifetime;
_logger = logger;
}
public override Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("上班了,又是精神抖擞的一天,output from StartAsync");
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
// 这里实现实际的业务逻辑
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await SomeMethodThatDoesTheWork(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Global exception occurred. Will resume in a moment.");
}
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
finally
{
_logger.LogWarning("Exiting application...");
GetOffWork(stoppingToken); //关闭前需要完成的工作
_hostApplicationLifetime.StopApplication(); //手动调用 StopApplication
}
}
private async Task SomeMethodThatDoesTheWork(CancellationToken cancellationToken)
{
if (_isStopping)
_logger.LogInformation("假装还在埋头苦干ing…… 其实我去洗杯子了");
else
_logger.LogInformation("我爱工作,埋头苦干ing……");
await Task.CompletedTask;
}
/// <summary>
/// 关闭前需要完成的工作
/// </summary>
private void GetOffWork(CancellationToken cancellationToken)
{
_logger.LogInformation("啊,糟糕,有一个紧急 bug 需要下班前完成!!!");
_logger.LogInformation("啊啊啊,我爱加班,我要再干 20 秒,Wait 1 ");
Task.Delay(TimeSpan.FromSeconds(20)).Wait();
_logger.LogInformation("啊啊啊啊啊啊,我爱加班,我要再干 1 分钟,Wait 2 ");
Task.Delay(TimeSpan.FromMinutes(1)).Wait();
_logger.LogInformation("啊哈哈哈哈哈,终于好了,下班走人!");
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("太好了,下班时间到了,output from StopAsync at: {time}", DateTimeOffset.Now);
_isStopping = true;
_logger.LogInformation("去洗洗茶杯先……", DateTimeOffset.Now);
Task.Delay(30_000).Wait();
_logger.LogInformation("茶杯洗好了。", DateTimeOffset.Now);
_logger.LogInformation("下班喽 ^_^", DateTimeOffset.Now);
return base.StopAsync(cancellationToken);
}
}
重新运行一下
dotnet build
dotnet run
然后按 CTRL+C 键关闭服务,看看运行结果是什么?
我们可以观察到在 Worker Service 启动和关闭时,基类 BackgroundService 中可重写的三个方法的运行顺序分别如下图所示:


总结
在本文中,我通过一个实例介绍了如何优雅退出 Worker Service 的相关知识。
Worker Service 本质上仍是一个控制台应用程序,执行一个作业。但它不仅可以作为控制台应用程序直接运行,也可以使用 sc.exe 实用工具安装为 Windows 服务,还可以部署到 linux 机器上作为后台进程运行。以后有时间我会介绍更多关于 Worker Service 的知识。
作者 : 技术译民
出品 : 技术译站
https://mp.weixin.qq.com/s/ujGkb5oaXq3lqX_g_eQ3_g .NET Worker Service 入门介绍 ︎
https://github.com/ITTranslate/WorkerServiceGracefullyShutdown 源码下载 ︎
.NET Worker Service 如何优雅退出的更多相关文章
- .NET Worker Service 作为 Windows 服务运行及优雅退出改进
上一篇文章我们了解了如何为 Worker Service 添加 Serilog 日志记录,今天我接着介绍一下如何将 Worker Service 作为 Windows 服务运行. 我曾经在前面一篇文章 ...
- .NET Worker Service 部署到 Linux 作为 Systemd Service 运行
上一篇文章我们了解了如何将.NET Worker Service 作为 Windows 服务运行,今天我接着介绍一下如何将 Worker Service 部署到 Linux 上,并作为 Systemd ...
- 在 ASP.NET Core和Worker Service中使用Quartz.Net
现在有了一个官方包Quartz.Extensions.Hosting实现使用Quartz.Net运行后台任务,所以把Quartz.Net添加到ASP.NET Core或Worker Service要简 ...
- 基于.Net Core 5.0 Worker Service 的 Quart 服务
前言 看过我之前博客的人应该都知道,我负责了相当久的部门数据同步相关的工作.其中的艰辛不赘述了. 随着需求的越来越复杂,最近windows的计划任务已经越发的不能满足我了,而且计划任务毕竟太弱智,总是 ...
- .NET Worker Service 添加 Serilog 日志记录
前面我们了解了 .NET Worker Service 的入门知识[1] 和 如何优雅退出 Worker Service [2],今天我们接着介绍一下如何为 Worker Service 添加 Ser ...
- Node 出现 uncaughtException 之后的优雅退出方案
Node 的异步特性是它最大的魅力,但是在带来便利的同时也带来了不少麻烦和坑,错误捕获就是一个.由于 Node 的异步特性,导致我们无法使用 try/catch 来捕获回调函数中的异常,例如: try ...
- 正确使用‘trap指令’实现Docker优雅退出
一般应用(比如mariadb)都会有一个退出命令,用户使用类似systemctl stop ****.service方法,停止其服务时,systemd会调用其配置文件注册的退出命令,该命令执行清理资源 ...
- NodeJS服务器退出:完成任务,优雅退出
上一篇文章,我们通过一个简单的例子,学习了NodeJS中对客户端的请求(request)对象的解析和处理,整个文件共享的功能已经完成.但是,纵观整个过程,还有两个地方明显需要改进: 首先,不能共享完毕 ...
- golang channel详解和协程优雅退出
非缓冲chan,读写对称 非缓冲channel,要求一端读取,一端写入.channel大小为零,所以读写操作一定要匹配. func main() { nochan := make(chan int) ...
随机推荐
- 对用pyinstaller打包的exe程序进行反编译,获得源码
参考文章: 1.https://www.cnblogs.com/DirWang/p/12018949.html#PyInstallerExtractor 2.https://msd.misuland. ...
- 13 条高效实用的 JavaScript 单行代码
JavaScript可以实现很多令人惊奇的事! 从复杂的框架到处理API,有太多的东西可以学习. 甚至,仅用一行代码,它也能完成一些很棒的工作. 不信?那么请看这13条JavaScript单行代码,用 ...
- [源码解析] 并行分布式框架 Celery 之 worker 启动 (2)
[源码解析] 并行分布式框架 Celery 之 worker 启动 (2) 目录 [源码解析] 并行分布式框架 Celery 之 worker 启动 (2) 0x00 摘要 0x01 前文回顾 0x2 ...
- windows一些知识
宽字节 1.什么是ASCII码? 一张存储了字母大小写与一些符号的表,用一个字节表示,最高位不使用,最多只能存储128个符号或字母,但世界上有很多种语言,这远远不够 2.什么是扩展ASCII码? 把最 ...
- shell字符串处理总结
1. 字符串切片 1.1 基于偏移量取字符串 返回字符串 string 的长度 ${#string} 示例 [root@centos8 script]#str=" I Love Python ...
- JS基础学习第四天
对象(Object) 对象是JS中的引用数据类型对象是一种复合数据类型,在对象中可以保存多个不同数据类型的属性使用typeof检查一个对象时,会返回object 对象的分类: 1.内建对象- 由ES标 ...
- 附031.Kubernetes_v1.20.4高可用部署架构二
kubeadm介绍 kubeadm概述 参考附003.Kubeadm部署Kubernetes. kubeadm功能 参考附003.Kubeadm部署Kubernetes. 本方案描述 本方案采用kub ...
- Dynamics CRM安装教程六:CRM产品安装
接下来就要开始进行CRM产品的安装了 首先要安装IIS,以及.NET FrameWork4.6及相关功能 打开服务器管理器,在添加角色功能向导中勾选IIS,点击添加功能 默认下一步 选择角色服务这里的 ...
- (十三)struts2的输入校验
输入校验是web应用必须处理的问题,要防止用户的误输入和恶意非法输入.struts2给我们提供了非常强大的输入校验体系. 输入校验分为客户端校验和服务器端校验.一般开发中两者都用,但是服务端校验必须使 ...
- python2爬取国家统计局全国省份城市区街道信息
工作中,再次需要python,发现python用得好 ,真的可以节省很多人力,先说我的需求,需要做一个类似像支付宝添加收货地址时,选择地区的功能,需要详细到街道信息,也就是4级联动,如右图.首先需要的 ...