.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://code.visualstudio.com/
- 最新的 .NET SDK://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 的知識。
作者 : 技術譯民
出品 : 技術譯站
-
//mp.weixin.qq.com/s/ujGkb5oaXq3lqX_g_eQ3_g .NET Worker Service 入門介紹 ↩︎
-
//github.com/ITTranslate/WorkerServiceGracefullyShutdown 源碼下載 ↩︎