.NET Worker Service 如何優(yōu)雅退出

上一篇文章中我們了解了 .NET 中的 Worker Service 入門介紹[1],今天我們接著介紹一下如何優(yōu)雅地關(guān)閉和退出 Worker Service。
Worker 類
從上一篇文章中,我們已經(jīng)知道了 Worker Service 模板為我們提供三個(gè)開箱即用的核心文件,其中 Worker 類是繼承自抽象基類 BackgroundService 的,而 BackgroundService 實(shí)現(xiàn)了 IHostedService 接口。最終 Worker 類會(huì)被注冊(cè)為托管服務(wù),我們處理任務(wù)的核心代碼就是寫在 Worker 類中的。所以,我們需要重點(diǎn)了解一下 Worker 及其基類。
先來(lái)看看它的基類 BackgroundService :

基類 BackgroundService 中有三個(gè)可重寫的方法,可以讓我們綁定到應(yīng)用程序的生命周期中:
抽象方法
ExecuteAsync:作為應(yīng)用程序主要入口點(diǎn)的方法。如果此方法退出,則應(yīng)用程序?qū)㈥P(guān)閉。我們必須在 Worker 中實(shí)現(xiàn)它。虛方法
StartAsync:在應(yīng)用程序啟動(dòng)時(shí)調(diào)用。如果需要,可以重寫此方法,它可用于在服務(wù)啟動(dòng)時(shí)一次性地設(shè)置資源;當(dāng)然,也可以忽略它。虛方法
StopAsync:在應(yīng)用程序關(guān)閉時(shí)調(diào)用。如果需要,可以重寫此方法,在關(guān)閉時(shí)釋放資源和銷毀對(duì)象;當(dāng)然,也可以忽略它。
默認(rèn)情況下 Worker 只重寫必要的抽象方法 ExecuteAsync。
新建一個(gè) Worker Service 項(xiàng)目
我們來(lái)新建一個(gè) Worker Service,使用 Task.Delay 來(lái)模擬關(guān)閉前必須完成的一些操作,看看是否可以通過(guò)簡(jiǎn)單地在 ExecuteAsync 中 Delay 來(lái)模擬實(shí)現(xiàn)優(yōu)雅關(guān)閉。
需要用到的開發(fā)工具:
Visual Studio Code:https://code.visualstudio.com/
最新的 .NET SDK:https://dotnet.microsoft.com/download
安裝好以上工具后,在終端中運(yùn)行以下命令,創(chuàng)建一個(gè) Worker Service 項(xiàng)目:
dotnet new Worker -n "MyService"
創(chuàng)建好 Worker Service 后,在 Visual Studio Code 中打開應(yīng)用程序,然后構(gòu)建并運(yùn)行一下,以確保一切正常:
dotnet build
dotnet run
按 CTRL+C 鍵關(guān)閉服務(wù),服務(wù)會(huì)立即退出,默認(rèn)情況下 Worker Service 的關(guān)閉就是這么直接!在很多場(chǎng)景(比如內(nèi)存中的隊(duì)列)中,這不是我們想要的結(jié)果,有時(shí)我們不得不在服務(wù)關(guān)閉前完成一些必要的資源回收或事務(wù)處理。
我們看一下 Worker 類的代碼,會(huì)看到它只重寫了基類 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);
}
}
我們嘗試修改一下此方法,退出前做一些業(yè)務(wù)處理:
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);
}
然后測(cè)試一下,看它是不是會(huì)像我們預(yù)期的那樣先等待 60 秒再關(guān)閉。
dotnet build
dotnet run
按 CTRL+C 鍵關(guān)閉服務(wù),我們會(huì)發(fā)現(xiàn),它在輸出 “等待退出” 后,并沒有等待 60 秒并輸出 “退出” 之后再關(guān)閉,而是很快便退出了。這就像我們熟悉的控制臺(tái)應(yīng)用程序,默認(rèn)情況下,在我們點(diǎn)了右上角的關(guān)閉按鈕或者按下 CTRL+C 鍵時(shí),會(huì)直接關(guān)閉一樣。
Worker Service 優(yōu)雅退出
那么,怎么才能實(shí)現(xiàn)優(yōu)雅退出呢?
方法其實(shí)很簡(jiǎn)單,那就是將 IHostApplicationLifetime 注入到我們的服務(wù)中,然后在應(yīng)用程序停止時(shí)手動(dòng)調(diào)用 IHostApplicationLifetime 的 StopApplication 方法來(lái)關(guān)閉應(yīng)用程序。
修改 Worker 的構(gòu)造函數(shù),注入 IHostApplicationLifetime:
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<Worker> _logger;
public Worker(IHostApplicationLifetime hostApplicationLifetime, ILogger<Worker> logger)
{
_hostApplicationLifetime = hostApplicationLifetime;
_logger = logger;
}
然后在 ExecuteAsync 中,處理完退出前必須完成的業(yè)務(wù)邏輯后,手動(dòng)調(diào)用 IHostApplicationLifetime 的 StopApplication 方法,下面是豐富過(guò)的 ExecuteAsync 代碼:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
// 這里實(shí)現(xiàn)實(shí)際的業(yè)務(wù)邏輯
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); //關(guān)閉前需要完成的工作
_hostApplicationLifetime.StopApplication(); //手動(dòng)調(diào)用 StopApplication
}
}
private async Task SomeMethodThatDoesTheWork(CancellationToken cancellationToken)
{
_logger.LogInformation("我愛工作,埋頭苦干ing……");
await Task.CompletedTask;
}
/// <summary>
/// 關(guān)閉前需要完成的工作
/// </summary>
private void GetOffWork(CancellationToken cancellationToken)
{
_logger.LogInformation("啊,糟糕,有一個(gè)緊急 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("啊哈哈哈哈哈,終于好了,下班走人!");
}
此時(shí),再次 dotnet run 運(yùn)行服務(wù),然后按 CTRL+C 鍵關(guān)閉服務(wù),您會(huì)發(fā)現(xiàn)關(guān)閉前需要完成的工作 GetOffWork 運(yùn)行完成后才會(huì)退出服務(wù)了。
至此,我們已經(jīng)實(shí)現(xiàn)了 Worker Service 的優(yōu)雅退出。
StartAsync 和 StopAsync
為了更進(jìn)一步了解 Worker Service,我們?cè)賮?lái)豐富一下我們的代碼,重寫基類 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
{
// 這里實(shí)現(xiàn)實(shí)際的業(yè)務(wù)邏輯
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); //關(guān)閉前需要完成的工作
_hostApplicationLifetime.StopApplication(); //手動(dòng)調(diào)用 StopApplication
}
}
private async Task SomeMethodThatDoesTheWork(CancellationToken cancellationToken)
{
if (_isStopping)
_logger.LogInformation("假裝還在埋頭苦干ing…… 其實(shí)我去洗杯子了");
else
_logger.LogInformation("我愛工作,埋頭苦干ing……");
await Task.CompletedTask;
}
/// <summary>
/// 關(guān)閉前需要完成的工作
/// </summary>
private void GetOffWork(CancellationToken cancellationToken)
{
_logger.LogInformation("啊,糟糕,有一個(gè)緊急 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("太好了,下班時(shí)間到了,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);
}
}
重新運(yùn)行一下
dotnet build
dotnet run
然后按 CTRL+C 鍵關(guān)閉服務(wù),看看運(yùn)行結(jié)果是什么?
我們可以觀察到在 Worker Service 啟動(dòng)和關(guān)閉時(shí),基類 BackgroundService 中可重寫的三個(gè)方法的運(yùn)行順序分別如下圖所示:


總結(jié)
在本文中,我通過(guò)一個(gè)實(shí)例介紹了如何優(yōu)雅退出 Worker Service 的相關(guān)知識(shí)。
Worker Service 本質(zhì)上仍是一個(gè)控制臺(tái)應(yīng)用程序,執(zhí)行一個(gè)作業(yè)。但它不僅可以作為控制臺(tái)應(yīng)用程序直接運(yùn)行,也可以使用 sc.exe 實(shí)用工具安裝為 Windows 服務(wù),還可以部署到 linux 機(jī)器上作為后臺(tái)進(jìn)程運(yùn)行。以后有時(shí)間我會(huì)介紹更多關(guān)于 Worker Service 的知識(shí)。
您可以從 GitHub 下載本文中的源碼[2]。
相關(guān)鏈接:
https://mp.weixin.qq.com/s/ujGkb5oaXq3lqX_g_eQ3_g .NET Worker Service 入門介紹 ??
https://github.com/ITTranslate/WorkerServiceGracefullyShutdown 源碼下載 ??
作者 :技術(shù)譯民
出品 :技術(shù)譯站(https://ITTranslator.cn/)
【推薦】.NET Core開發(fā)實(shí)戰(zhàn)視頻課程 ★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門篇-開篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴注入與動(dòng)態(tài)選擇看這篇就夠了
10個(gè)小技巧助您寫出高性能的ASP.NET Core代碼
用abp vNext快速開發(fā)Quartz.NET定時(shí)任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說(shuō)法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
