建立佇列服務

佇列服務是長時間運行的服務的一個絕佳範例,工作項目可以排隊,並在先前的工作項目完成後依次處理。 依賴 Worker Service 模板,您可以在 BackgroundService 上建構新功能。

在本教學課程中,您將瞭解如何:

  • 建立佇列服務。
  • 將工作委派到任務佇列。
  • IHostApplicationLifetime 事件註冊一個主控台按鍵監聽器。

小提示

所有 「.NET 工作者」範例原始程式碼都可在 範例瀏覽器 下載。 如需詳細資訊,請參閱 瀏覽程式碼範例:在 .NET 中的工作者

先決條件

建立新專案

若要使用 Visual Studio 建立新的背景工作服務專案,請選取 [檔案]>[新增>專案...]。從 [[建立新專案] 對話框搜尋 [背景工作服務],然後選取 [背景工作服務] 範本。 如果您想要使用 .NET CLI,請在工作目錄中開啟您最愛的終端機。 執行 dotnet new 命令,並將 <Project.Name> 替換為您想要的專案名稱。

dotnet new worker --name <Project.Name>

如需 .NET CLI 新建 Worker Service 專案命令的詳細資訊,請參閱 dotnet new worker

小提示

如果您使用 Visual Studio Code,您可以從整合式終端機執行 .NET CLI 命令。 如需詳細資訊,請參閱 Visual Studio Code:整合式終端機

建立排隊服務

你可能對QueueBackgroundWorkItem(Func<CancellationToken,Task>)功能來自System.Web.Hosting命名空間很熟悉。

小提示

命名空間的功能 System.Web 刻意未移植至 .NET,且仍僅支援 .NET 框架。 欲了解更多資訊,請參閱「 從增量 ASP.NET 開始 ASP.NET 核心遷移」。

在 .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;
    }
}

前述實作依賴 a Channel<T> 作為隊列。 BoundedChannelOptions(Int32) 被調用時使用指定的容量。 容量應根據預期的應用程式負載及同時存取佇列的執行緒數量來設定。 BoundedChannelFullMode.WaitChannelWriter<T>.WriteAsync 返回一個任務,這任務只有在空間出現時才會完成。 這會導致反壓,當過多的發佈者或呼叫開始累積時。

重寫工人階級

在下列 QueueHostedService 範例中:

  • ProcessTaskQueueAsync 方法回傳 a TaskExecuteAsync
  • ProcessTaskQueueAsync 中,佇列中的背景工作會從佇列清除並執行。
  • StopAsync 中的服務停止前,工作項目會等候。

將現有 Worker 類別替換成以下 C# 程式碼,並將檔案重新命名為 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);
    }
}

每次在輸入裝置上選取 MonitorLoop 金鑰時,w 服務都會處理託管服務的加入佇列工作:

  • IBackgroundTaskQueue 會插入 MonitorLoop 服務中。
  • 會呼叫 IBackgroundTaskQueue.QueueBackgroundWorkItemAsync 以將工作項目加入佇列。
  • 工作項目會模擬長時間執行的背景工作:
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);
        }
    }
}

將現有 Program 內容替換為以下 C# 程式碼:

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 擴展方法註冊的。 MonitorLoopProgram.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 來表示取消。

另請參閱