建立佇列服務
佇列服務正是一種長時間執行的服務,工作項目可以排入佇列,然後在先前的工作項目時循序處理。 您可以仰賴背景工作角色服務範本,在 BackgroundService 之上建置新功能。
在本教學課程中,您會了解如何:
- 建立佇列服務。
- 將工作委派給工作佇列。
- 從 IHostApplicationLifetime 事件註冊主控台金鑰接聽程式。
提示
範例瀏覽器中提供所有「.NET 中的背景工作角色」範例原始程式碼以供下載。 如需詳細資訊,請參閱瀏覽程式碼範例:.NET 中的背景工作角色。
必要條件
- .NET 8.0 SDK 或更新版本
- .NET 整合式開發環境 (IDE)
- 歡迎使用 Visual Studio
建立新專案
若要使用 Visual Studio Code 建立新的背景工作服務專案,您將選取 [檔案]>[新增]>[專案...]。從 [建立新專案] 對話方塊搜尋 [背景工作服務],然後選取 [背景工作服務] 範本。 如果您想要使用 .NET CLI,請在工作目錄中開啟您慣用的終端。 執行 dotnet new
命令,並以您想要的專案名稱取代 <Project.Name>
。
dotnet new worker --name <Project.Name>
如需 .NET CLI 新背景工作服務專案命令的詳細資訊,請參閱 dotnet new worker。
提示
如果您使用 Visual Studio Code,您可以從整合式終端執行 .NET CLI 命令。 如需詳細資訊,請參閱 Visual Studio Code:整合式終端。
建立佇列服務
您可能已熟悉 System.Web.Hosting
命名空間中的 QueueBackgroundWorkItem(Func<CancellationToken,Task>) 功能。
提示
System.Web
命名空間的功能是刻意不會移植到 .NET,而且會保留為 .NET Framework 的獨佔。 如需詳細資訊,請參閱開始使用從 ASP.NET 到 ASP.NET Core 的累加式移轉。
在 .NET 中,若要為受到 QueueBackgroundWorkItem
功能啟發的服務建立模型,請先將 IBackgroundTaskQueue
介面新增至專案:
namespace App.QueueService;
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(
Func<CancellationToken, ValueTask> workItem);
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken);
}
有兩種方法,一種會公開佇列功能,另一種會清除先前佇列的工作項目。 「工作項目」是 Func<CancellationToken, ValueTask>
。 接下來,將預設實作新增至專案。
using System.Threading.Channels;
namespace App.QueueService;
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)
{
ArgumentNullException.ThrowIfNull(workItem);
await _queue.Writer.WriteAsync(workItem);
}
public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken)
{
Func<CancellationToken, ValueTask>? workItem =
await _queue.Reader.ReadAsync(cancellationToken);
return workItem;
}
}
上述實作依賴 Channel<T> 作為佇列。 BoundedChannelOptions(Int32) 會以明確的容量呼叫。 容量應該根據預期的應用程式負載和存取佇列的並行執行緒數目來設定。 BoundedChannelFullMode.Wait 會導致 ChannelWriter<T>.WriteAsync 的呼叫傳回工作,只在有空間可用時才完成。 如果有太多發行者/呼叫開始累積,這會造成反向壓力。
重寫背景工作角色類別
在下列 QueueHostedService
範例中:
ProcessTaskQueueAsync
方法會在ExecuteAsync
中傳回 Task。- 在
ProcessTaskQueueAsync
中,佇列中的背景工作會從佇列清除並執行。 - 在
StopAsync
中的服務停止前,工作項目會等候。
以下列 C# 程式碼取代現有的 Worker
類別,並將檔案重新命名為 QueueHostedService.cs。
namespace App.QueueService;
public sealed class QueuedHostedService(
IBackgroundTaskQueue taskQueue,
ILogger<QueuedHostedService> logger) : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("""
{Name} is running.
Tap W to add a work item to the
background queue.
""",
nameof(QueuedHostedService));
return ProcessTaskQueueAsync(stoppingToken);
}
private async Task ProcessTaskQueueAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
Func<CancellationToken, ValueTask>? workItem =
await taskQueue.DequeueAsync(stoppingToken);
await workItem(stoppingToken);
}
catch (OperationCanceledException)
{
// Prevent throwing if stoppingToken was signaled
}
catch (Exception ex)
{
logger.LogError(ex, "Error occurred executing task work item.");
}
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
logger.LogInformation(
$"{nameof(QueuedHostedService)} is stopping.");
await base.StopAsync(stoppingToken);
}
}
每次在輸入裝置上選取 w
金鑰時,MonitorLoop
服務都會處理託管服務的加入佇列工作:
IBackgroundTaskQueue
會插入MonitorLoop
服務中。- 會呼叫
IBackgroundTaskQueue.QueueBackgroundWorkItemAsync
以將工作項目加入佇列。 - 工作項目會模擬長時間執行的背景工作:
- Delay 執行三次 5 秒的延遲。
- 如果取消工作,
try-catch
陳述式會截獲 OperationCanceledException。
namespace App.QueueService;
public sealed class MonitorLoop(
IBackgroundTaskQueue taskQueue,
ILogger<MonitorLoop> logger,
IHostApplicationLifetime applicationLifetime)
{
private readonly CancellationToken _cancellationToken = applicationLifetime.ApplicationStopping;
public void StartMonitorLoop()
{
logger.LogInformation($"{nameof(MonitorAsync)} loop is starting.");
// Run a console user input loop in a background thread
Task.Run(async () => await MonitorAsync());
}
private async ValueTask MonitorAsync()
{
while (!_cancellationToken.IsCancellationRequested)
{
var keyStroke = Console.ReadKey();
if (keyStroke.Key == ConsoleKey.W)
{
// Enqueue a background work item
await taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItemAsync);
}
}
}
private async ValueTask BuildWorkItemAsync(CancellationToken token)
{
// Simulate three 5-second tasks to complete
// for each enqueued work item
int delayLoop = 0;
var guid = Guid.NewGuid();
logger.LogInformation("Queued work item {Guid} is starting.", guid);
while (!token.IsCancellationRequested && delayLoop < 3)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
catch (OperationCanceledException)
{
// Prevent throwing if the Delay is cancelled
}
++ delayLoop;
logger.LogInformation("Queued work item {Guid} is running. {DelayLoop}/3", guid, delayLoop);
}
if (delayLoop is 3)
{
logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
}
else
{
logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
}
}
}
使用下列 C# 程式碼取代目前 Program
的內容:
using App.QueueService;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<MonitorLoop>();
builder.Services.AddHostedService<QueuedHostedService>();
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ =>
{
if (!int.TryParse(builder.Configuration["QueueCapacity"], out var queueCapacity))
{
queueCapacity = 100;
}
return new DefaultBackgroundTaskQueue(queueCapacity);
});
IHost host = builder.Build();
MonitorLoop monitorLoop = host.Services.GetRequiredService<MonitorLoop>()!;
monitorLoop.StartMonitorLoop();
host.Run();
這些服務會在 (Program.cs) 中註冊。 託管服務使用 AddHostedService
擴充方法註冊。 MonitorLoop
會在 Program.cs 最上層陳述式中啟動:
MonitorLoop monitorLoop = host.Services.GetRequiredService<MonitorLoop>()!;
monitorLoop.StartMonitorLoop();
如需註冊服務的詳細資訊,請參閱 .NET 中的相依性插入。
驗證服務功能
若要從 Visual Studio 執行應用程式,請選取 F5 或選取 [偵錯]> [開始偵錯] 功能表選項。 如果您使用的是 .NET CLI,請從工作目錄執行 dotnet run
命令:
dotnet run
如需 .NET CLI 執行命令的詳細資訊,請參閱 dotnet run。
出現提示時,至少輸入 w
(或 W
) 一次,以將模擬的工作項目排入佇列,如範例輸出所示:
info: App.QueueService.MonitorLoop[0]
MonitorAsync loop is starting.
info: App.QueueService.QueuedHostedService[0]
QueuedHostedService is running.
Tap W to add a work item to the background queue.
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: .\queue-service
winfo: App.QueueService.MonitorLoop[0]
Queued work item 8453f845-ea4a-4bcb-b26e-c76c0d89303e is starting.
info: App.QueueService.MonitorLoop[0]
Queued work item 8453f845-ea4a-4bcb-b26e-c76c0d89303e is running. 1/3
info: App.QueueService.MonitorLoop[0]
Queued work item 8453f845-ea4a-4bcb-b26e-c76c0d89303e is running. 2/3
info: App.QueueService.MonitorLoop[0]
Queued work item 8453f845-ea4a-4bcb-b26e-c76c0d89303e is running. 3/3
info: App.QueueService.MonitorLoop[0]
Queued Background Task 8453f845-ea4a-4bcb-b26e-c76c0d89303e is complete.
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
info: App.QueueService.QueuedHostedService[0]
QueuedHostedService is stopping.
如果從 Visual Studio 內執行應用程式,請選取 [偵錯]>[停止偵錯...]。或者,從主控台視窗選取 Ctrl + C 以發出取消訊號。