背景服務執行計時工作 - 使用 BackgroundService 與 PeriodicTimer

  • 696
  • 0
  • C#
  • 2024-10-21

背景常駐執行計時工作的實作方式有很多種,而我習慣在 ASP.NET Coe Web Application 使用 BackgroundService 然後搭配 Task.Delay 的方式來完成計時執行工作的處理,而在 .NET 6 提供了 PeriodicTimer 後就可以更方便的處理計時工作,這篇就來認識執行計時工作的幾種實作方式。

BackgroundService

先來幾個連結好好認識 BackgroundService

這裡就使用 BackgroundService 建立一個背景常駐服務,並且定時執行簡單的工作處理

namespace Sample.WebApplication.BackgroundServices;

/// <summary>
/// class SampleBackgroundService
/// </summary>
public class SampleBackgroundService : BackgroundService
{
    /// <summary>
    /// 執行間隔時間 60 sec
    /// </summary>
    private static readonly TimeSpan IntervalTime = TimeSpan.FromSeconds(60);
    
    private readonly ILogger<SampleBackgroundService> _logger;
    
    private readonly TimeProvider _timeProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="SampleBackgroundService"/> class
    /// </summary>
    /// <param name="logger">The logger</param>
    /// <param name="timeProvider">The timeProvider</param>
    public SampleBackgroundService(ILogger<SampleBackgroundService> logger, TimeProvider timeProvider)
    {
        this._logger = logger;
        this._timeProvider = timeProvider;
    }
    
    /// <summary>
    /// Executes the stopping token
    /// </summary>
    /// <param name="stoppingToken">The stopping token</param>
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        this._logger.LogInformation(
            "{DateTimeNow} [{MachineName}] {TypeName} is starting.",
            $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
            Environment.MachineName,
            this.GetType().Name);

        while (stoppingToken.IsCancellationRequested is false)
        {
            // 放在前面則是先延遲 60 秒,然後再處理工作執行
            await Task.Delay(IntervalTime, stoppingToken).ConfigureAwait(false);

            this._logger.LogInformation(
                "{DateTimeNow} [{MachineName}] {TypeName} Processing.",
                $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
                Environment.MachineName,
                this.GetType().Name);
            
            // 如果是放在後面,則是先處理工作後再延遲 60 秒
            // await Task.Delay(IntervalTime, stoppingToken).ConfigureAwait(false);
        }
    }

    /// <summary>
    /// Stops the cancellation token
    /// </summary>
    /// <param name="cancellationToken">The cancellation token</param>
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        this._logger.LogInformation(
            "{DateTimeNow} [{MachineName}] {TypeName} is stopping.",
            $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
            Environment.MachineName,
            this.GetType().Name);
        await base.StopAsync(cancellationToken);
    }
}

做好 SampleBackgroundService 後,記得要再去 Program.cs 裡註冊背景服務

// 註冊背景服務
builder.Services.AddHostedService<SampleBackgroundService>();

觀察 Web Application 執行,在程式啟動時就有寫入 log,然後於執行後每間隔 60 秒就會再寫 log,最後當 Web Application 停止服務時寫入 log

以上是使用 Task.Delay 的方式去完成間隔一段時間的計時處理方式,接下來就換成 PeriodicTimer 來試試看。

 

PeriodicTimer

這是在 .NET 6 以後所提供的類別,透過以下的連結認識這個類別

PeriodicTimer 與 Timer 的差異在於 PeriodicTimer  提供了 WaitForNextTickAsync(CancellationToken) 方法,是非同步等待處理,而不是使用 Timer 類別去指定 callback 的方式。

將 SampleBackgroundService  改使用 PeriodicTimer

namespace Sample.WebApplication.BackgroundServices;

/// <summary>
/// class SampleBackgroundService
/// </summary>
public class SampleBackgroundService : BackgroundService
{
    /// <summary>
    /// 間隔時間 60 sec
    /// </summary>
    private static readonly TimeSpan IntervalTime = TimeSpan.FromSeconds(60);

    private readonly ILogger<SampleBackgroundService> _logger;

    private readonly TimeProvider _timeProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="SampleBackgroundService"/> class
    /// </summary>
    /// <param name="logger">The logger</param>
    /// <param name="timeProvider">The timeProvider</param>
    public SampleBackgroundService(ILogger<SampleBackgroundService> logger, TimeProvider timeProvider)
    {
        this._logger = logger;
        this._timeProvider = timeProvider;
    }

    /// <summary>
    /// Executes the stopping token
    /// </summary>
    /// <param name="stoppingToken">The stopping token</param>
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        this._logger.LogInformation(
            "{DateTimeNow} [{MachineName}] {TypeName} is starting.",
            $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
            Environment.MachineName,
            this.GetType().Name);

        // 使用 PeriodicTimer
        using PeriodicTimer periodicTimer = new(IntervalTime);

        while (await periodicTimer.WaitForNextTickAsync(stoppingToken) && stoppingToken.IsCancellationRequested is false)
        {
            this._logger.LogInformation(
                "{DateTimeNow} [{MachineName}] {TypeName} Processing.",
                $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
                Environment.MachineName,
                this.GetType().Name);
        }
    }

    /// <summary>
    /// Stops the cancellation token
    /// </summary>
    /// <param name="cancellationToken">The cancellation token</param>
    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        this._logger.LogInformation(
            "{DateTimeNow} [{MachineName}] {TypeName} is stopping.",
            $"{this._timeProvider.GetLocalNow().DateTime:yyyy-MM-dd HH:mm:ss}",
            Environment.MachineName,
            this.GetType().Name);
        await base.StopAsync(cancellationToken);
    }
}

修改後觀察 Web Application 執行,在程式啟動時就有寫入 log,然後於執行後每間隔 60 秒就會再寫 log,最後當 Web Application 停止服務時寫入 log

 

原本還想再帶點其他東西,不過這一篇就先到這裡。

以上