.NET Worker Service 如何優雅退出

上一篇文章中我們了解了 .NET Worker Service 的入門知識[1],今天我們接著介紹一下如何優雅地關閉和退出 Worker Service。

Worker 類

上一篇文章中,我們已經知道了 Worker Service 模板為我們提供三個開箱即用的核心文件,其中 Worker 類是繼承自抽象基類 BackgroundService 的,而 BackgroundService 實現了 IHostedService 介面。最終 Worker 類會被註冊為託管服務,我們處理任務的核心程式碼就是寫在 Worker 類中的。所以,我們需要重點了解一下 Worker 及其基類。

先來看看它的基類 BackgroundService

Background Service

基類 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 startup flowchart

worker service shutdown flowchart

總結

在本文中,我通過一個實例介紹了如何優雅退出 Worker Service 的相關知識。

Worker Service 本質上仍是一個控制台應用程式,執行一個作業。但它不僅可以作為控制台應用程式直接運行,也可以使用 sc.exe 實用工具安裝為 Windows 服務,還可以部署到 linux 機器上作為後台進程運行。以後有時間我會介紹更多關於 Worker Service 的知識。

您可以從 GitHub 下載本文中的源碼[2]

作者 : 技術譯民
出品 : 技術譯站


  1. //mp.weixin.qq.com/s/ujGkb5oaXq3lqX_g_eQ3_g .NET Worker Service 入門介紹 ↩︎

  2. //github.com/ITTranslate/WorkerServiceGracefullyShutdown 源碼下載 ↩︎