上一篇文章中我们了解了 .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. java例题 判断一个数能被几个9整除

    有点懵,被几个9整除,我理解的是n=n/9能整除几次,代码如下: 1 /*45 [程序 45 被 9 整除] 2 题目:判断一个数能被几个 9 整除 3 */ 4 5 /*分析 6 * 1.用whil ...

  2. K8S 本地 配置 Local PV 实践

    上面我们创建了后端是 hostPath 类型的 PV 资源对象,我们也提到了,使用 hostPath 有一个局限性就是,我们的 Pod 不能随便漂移,需要固定到一个节点上,因为一旦漂移到其他节点上去了 ...

  3. 【oracle学习笔记01】oracle architecture —— Memory Strucrure

    附图3: granule_size for each components 附图4:

  4. [.net] 关于Exception的几点思考和在项目中的使用(三)

    本文链接: https://www.cnblogs.com/hubaijia/p/about-exceptions-3.html 系列文章: 关于Exception的几点思考和在项目中的使用(一) 关 ...

  5. Go-18-自动下载所有依赖包

    如何自动下载所有依赖包? 大部分情况下大家下载 Go 项目都是使用go get命令,它除了会下载指定的项目代码,还会去下载这个项目所依赖的所有项目. 但是有的时候我们的项目由于各种原因并不是通过go ...

  6. ES6 第二天

    三点运算符 <script type="text/javascript"> function func(...params){ params.forEach(funct ...

  7. 【Azure Redis 缓存 Azure Cache For Redis】Redis连接池

    问题描述 Redis根据定价层说明,不同级别支持的连接数最多可达4万(同时),但是当短时间又大量连接请求建立的时候,Redis服务的服务压力非常大,到达100%.严重影响了高响应的要求.最严重时,经常 ...

  8. mooc人大单元测试1

    @font-face { font-family: Wingdings } @font-face { font-family: 宋体 } @font-face { font-family: " ...

  9. 老学长的TODOLIST

    初期: 一.基本算法: (1)枚举(poj1753,poj2965) (2)贪心(poj1328,poj2109,poj2586) (3)递归和分治法 (4)递推 (5)构造法(poj3295)(这种 ...

  10. 6- MySQL条件查询与通配符

    -数据库表中一般包含大量的数据,很少需要查询表中的所有行,通常只会根据特定的需要提取表中的一部分数据.这个时候需要制定查询条件,及过滤条件. -在select语句中,数据库根据where字句中指定的查 ...