有些 API 要做許多任務,例如讀寫資料庫、發送 API 與其他系統溝通、寄送 Email 和簡訊等等,有些外部服務從呼叫到處理完事情再收到對方回應需要一段時間,如果等全部都結束再回復那個調用我們 API 的使用者,對方可能都斷線甚至睡著了,最好的方式就是不等待某些比較不重要的服務就直接回應,例如發送 Email ,不需要等到真的發送成功了再回應 API 。我們可以把「要發送 Email」這件事放入佇列,直接回應 API ,然後慢慢寄送 Email 即可。
在之前 最詳細 C# 使用 .NET 6 的 Worker Service 專案快速建立 Windows 服務教學 這篇有提到使用 BackgroundService 達成,本篇也是要使用 BackgroundService ,把要處理的事項放入佇列後讓 BackgroundService 自己慢慢處理
微軟官方有給出相關範例,但是不知道是不是每到半夜就頭昏昏研究了好久才搞懂。下方範例是建立 MyBackgroundService 背景服務,會一直等待佇列中出現任務後處理,處理完後持續等待下一個任務,直到任務中止。
在 Program.cs 註冊用於處理的 MyBackgroundService 背景服務和 IBackgroundTaskQueue 單例介面:
在 ASP.NET Core 中的其他服務中就可以使用下列方式將任務加入佇列中
在 QueueBackgroundWorkItemAsync 中的 token 參數是什麼呢?為了讓我們可以處理任務被要求取消的情況,在下面的程式碼中,如果執行到一半把程式關掉,就會出現「任務被要求取消」,就可以讓程式在可以被停止的時候關閉。
延伸閱讀:
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
在之前 最詳細 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
感謝教學~
回覆刪除非常感謝您的鼓勵!我會繼續寫下去
刪除System.Threading.Channels
回覆刪除https://learn.microsoft.com/zh-tw/dotnet/core/extensions/channels
非常感謝您的建議!
刪除