上一篇文章中我们了解了 .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 来模拟关闭前必须完成的一些操作,看看是否可以通过简单地在 ExecuteAsyncDelay 来模拟实现优雅关闭。

需要用到的开发工具:

安装好以上工具后,在终端中运行以下命令,创建一个 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 注入到我们的服务中,然后在应用程序停止时手动调用 IHostApplicationLifetimeStopApplication 方法来关闭应用程序。

修改 Worker 的构造函数,注入 IHostApplicationLifetime

private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<Worker> _logger; public Worker(IHostApplicationLifetime hostApplicationLifetime, ILogger<Worker> logger)
{
_hostApplicationLifetime = hostApplicationLifetime;
_logger = logger;
}

然后在 ExecuteAsync 中,处理完退出前必须完成的业务逻辑后,手动调用 IHostApplicationLifetimeStopApplication 方法,下面是丰富过的 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,我们再来丰富一下我们的代码,重写基类 BackgroundServiceStartAsyncStopAsync 方法:

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 的知识。

您可以从 GitHub 下载本文中的源码[2]

作者 : 技术译民

出品 : 技术译站


  1. https://mp.weixin.qq.com/s/ujGkb5oaXq3lqX_g_eQ3_g .NET Worker Service 入门介绍

  2. https://github.com/ITTranslate/WorkerServiceGracefullyShutdown 源码下载

.NET Worker Service 如何优雅退出的更多相关文章

  1. .NET Worker Service 作为 Windows 服务运行及优雅退出改进

    上一篇文章我们了解了如何为 Worker Service 添加 Serilog 日志记录,今天我接着介绍一下如何将 Worker Service 作为 Windows 服务运行. 我曾经在前面一篇文章 ...

  2. .NET Worker Service 部署到 Linux 作为 Systemd Service 运行

    上一篇文章我们了解了如何将.NET Worker Service 作为 Windows 服务运行,今天我接着介绍一下如何将 Worker Service 部署到 Linux 上,并作为 Systemd ...

  3. 在 ASP.NET Core和Worker Service中使用Quartz.Net

    现在有了一个官方包Quartz.Extensions.Hosting实现使用Quartz.Net运行后台任务,所以把Quartz.Net添加到ASP.NET Core或Worker Service要简 ...

  4. 基于.Net Core 5.0 Worker Service 的 Quart 服务

    前言 看过我之前博客的人应该都知道,我负责了相当久的部门数据同步相关的工作.其中的艰辛不赘述了. 随着需求的越来越复杂,最近windows的计划任务已经越发的不能满足我了,而且计划任务毕竟太弱智,总是 ...

  5. .NET Worker Service 添加 Serilog 日志记录

    前面我们了解了 .NET Worker Service 的入门知识[1] 和 如何优雅退出 Worker Service [2],今天我们接着介绍一下如何为 Worker Service 添加 Ser ...

  6. Node 出现 uncaughtException 之后的优雅退出方案

    Node 的异步特性是它最大的魅力,但是在带来便利的同时也带来了不少麻烦和坑,错误捕获就是一个.由于 Node 的异步特性,导致我们无法使用 try/catch 来捕获回调函数中的异常,例如: try ...

  7. 正确使用‘trap指令’实现Docker优雅退出

    一般应用(比如mariadb)都会有一个退出命令,用户使用类似systemctl stop ****.service方法,停止其服务时,systemd会调用其配置文件注册的退出命令,该命令执行清理资源 ...

  8. NodeJS服务器退出:完成任务,优雅退出

    上一篇文章,我们通过一个简单的例子,学习了NodeJS中对客户端的请求(request)对象的解析和处理,整个文件共享的功能已经完成.但是,纵观整个过程,还有两个地方明显需要改进: 首先,不能共享完毕 ...

  9. golang channel详解和协程优雅退出

    非缓冲chan,读写对称 非缓冲channel,要求一端读取,一端写入.channel大小为零,所以读写操作一定要匹配. func main() { nochan := make(chan int) ...

随机推荐

  1. 对用pyinstaller打包的exe程序进行反编译,获得源码

    参考文章: 1.https://www.cnblogs.com/DirWang/p/12018949.html#PyInstallerExtractor 2.https://msd.misuland. ...

  2. 13 条高效实用的 JavaScript 单行代码

    JavaScript可以实现很多令人惊奇的事! 从复杂的框架到处理API,有太多的东西可以学习. 甚至,仅用一行代码,它也能完成一些很棒的工作. 不信?那么请看这13条JavaScript单行代码,用 ...

  3. [源码解析] 并行分布式框架 Celery 之 worker 启动 (2)

    [源码解析] 并行分布式框架 Celery 之 worker 启动 (2) 目录 [源码解析] 并行分布式框架 Celery 之 worker 启动 (2) 0x00 摘要 0x01 前文回顾 0x2 ...

  4. windows一些知识

    宽字节 1.什么是ASCII码? 一张存储了字母大小写与一些符号的表,用一个字节表示,最高位不使用,最多只能存储128个符号或字母,但世界上有很多种语言,这远远不够 2.什么是扩展ASCII码? 把最 ...

  5. shell字符串处理总结

    1. 字符串切片 1.1 基于偏移量取字符串 返回字符串 string 的长度 ${#string} 示例 [root@centos8 script]#str=" I Love Python ...

  6. JS基础学习第四天

    对象(Object) 对象是JS中的引用数据类型对象是一种复合数据类型,在对象中可以保存多个不同数据类型的属性使用typeof检查一个对象时,会返回object 对象的分类: 1.内建对象- 由ES标 ...

  7. 附031.Kubernetes_v1.20.4高可用部署架构二

    kubeadm介绍 kubeadm概述 参考附003.Kubeadm部署Kubernetes. kubeadm功能 参考附003.Kubeadm部署Kubernetes. 本方案描述 本方案采用kub ...

  8. Dynamics CRM安装教程六:CRM产品安装

    接下来就要开始进行CRM产品的安装了 首先要安装IIS,以及.NET FrameWork4.6及相关功能 打开服务器管理器,在添加角色功能向导中勾选IIS,点击添加功能 默认下一步 选择角色服务这里的 ...

  9. (十三)struts2的输入校验

    输入校验是web应用必须处理的问题,要防止用户的误输入和恶意非法输入.struts2给我们提供了非常强大的输入校验体系. 输入校验分为客户端校验和服务器端校验.一般开发中两者都用,但是服务端校验必须使 ...

  10. python2爬取国家统计局全国省份城市区街道信息

    工作中,再次需要python,发现python用得好 ,真的可以节省很多人力,先说我的需求,需要做一个类似像支付宝添加收货地址时,选择地区的功能,需要详细到街道信息,也就是4级联动,如右图.首先需要的 ...