ASP.NET Core 使用 BackgroundService 建立背景服務佇列 示範

有些 API 要做許多任務,例如讀寫資料庫、發送 API 與其他系統溝通、寄送 Email 和簡訊等等,有些外部服務從呼叫到處理完事情再收到對方回應需要一段時間,如果等全部都結束再回復那個調用我們 API 的使用者,對方可能都斷線甚至睡著了,最好的方式就是不等待某些比較不重要的服務就直接回應,例如發送 Email ,不需要等到真的發送成功了再回應 API 。我們可以把「要發送 Email」這件事放入佇列,直接回應 API ,然後慢慢寄送 Email 即可。

在之前 最詳細 C# 使用 .NET 6 的 Worker Service 專案快速建立 Windows 服務教學 這篇有提到使用 BackgroundService 達成,本篇也是要使用 BackgroundService ,把要處理的事項放入佇列後讓 BackgroundService 自己慢慢處理

微軟官方有給出相關範例,但是不知道是不是每到半夜就頭昏昏研究了好久才搞懂。下方範例是建立 MyBackgroundService 背景服務,會一直等待佇列中出現任務後處理,處理完後持續等待下一個任務,直到任務中止。
    
using System.Threading.Channels;
    
public class MyBackgroundService : BackgroundService
{
    private readonly ILogger<MyBackgroundService> _logger;
    private readonly IBackgroundTaskQueue _taskQueue;

    public MyBackgroundService(
        ILogger<MyBackgroundService> logger,
        IBackgroundTaskQueue taskQueue
    )
    {
        _logger = logger;
        _taskQueue = taskQueue;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                Func<CancellationToken, ValueTask> workItem = await _taskQueue.DequeueAsync(stoppingToken);
                await workItem(stoppingToken);
            }
            catch (OperationCanceledException)
            {
                _logger.LogInformation("工作已取消");
            }
            catch (Exception e)
            {
                _logger.LogError(e, "執行工作時發生錯誤");
            }
        }
    }
}

public interface IBackgroundTaskQueue
{
    /// <summary>
    /// 佇列增加非同步任務
    /// </summary>
    /// <param name="workItem"></param>
    /// <returns></returns>
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    /// <summary>
    /// 非同步取得任務
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken);
}

public sealed class DefaultBackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public DefaultBackgroundTaskQueue(int capacity)
    {
        BoundedChannelOptions options = new(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem is null) throw new ArgumentNullException(nameof(workItem));
        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken)
    {
        return await _queue.Reader.ReadAsync(cancellationToken);
    }
}
    

在 Program.cs 註冊用於處理的 MyBackgroundService 背景服務和 IBackgroundTaskQueue 單例介面:
    
var builder = WebApplication.CreateBuilder(args);


builder.Services.AddHostedService<MyBackgroundService>();
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ =>
{
    // 嘗試從設定檔中取得 QueueCapacity 參數,用來設定最大容量,預設值為 100
    if (!int.TryParse(builder.Configuration["QueueCapacity"], out var queueCapacity))
    {
        queueCapacity = 100;
    }

    return new DefaultBackgroundTaskQueue(queueCapacity);
});


var app = builder.Build();
    

在 ASP.NET Core 中的其他服務中就可以使用下列方式將任務加入佇列中
    
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
    private readonly IBackgroundTaskQueue  _taskQueue;

    public TestController(IBackgroundTaskQueue  taskQueue)
    {
        _taskQueue = taskQueue;
    }
    
    [HttpGet]
    public ActionResult Get()
    {
        _taskQueue.QueueBackgroundWorkItemAsync(async token =>
        {
            await Task.Delay(1000, token);
            Console.WriteLine("Hello World!");
        });
        
        return Ok();
    }
}
    

在 QueueBackgroundWorkItemAsync 中的 token 參數是什麼呢?為了讓我們可以處理任務被要求取消的情況,在下面的程式碼中,如果執行到一半把程式關掉,就會出現「任務被要求取消」,就可以讓程式在可以被停止的時候關閉。
    
_taskQueue.QueueBackgroundWorkItemAsync(async token =>
{
    await Task.Delay(1000);
    if (token.IsCancellationRequested)
    {
        Console.WriteLine("任務被要求取消");
        return;
    }

    Console.WriteLine("Hello World!");
});
    



延伸閱讀:
System.Threading.Channels 多執行緒佇列 示範

參考資料:
Microsoft.Learn - Implement background tasks in microservices with IHostedService and the BackgroundService class
Microsoft.Learn - Create a Queue Service
JetBrains.Blog - How to start using .NET Background Services
StackOverflow - .Net Core Queue Background Tasks
Queuing with no complexity: get to know the Background Service Queue .NET

留言

  1. 回覆
    1. 非常感謝您的鼓勵!我會繼續寫下去

      刪除
  2. System.Threading.Channels
    https://learn.microsoft.com/zh-tw/dotnet/core/extensions/channels

    回覆刪除

張貼留言

如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com