Erstellen eines Warteschlangendiensts

Ein Warteschlangendienst ist ein gutes Beispiel für einen Dienst mit langer Ausführungsdauer, bei dem Arbeitselemente in die Warteschlange eingereiht und sequenziell bearbeitet werden können, wenn vorherige Arbeitselemente abgeschlossen sind. Mit der Workerdienstvorlage erstellen Sie neue Funktionen auf der Grundlage von BackgroundService.

In diesem Tutorial lernen Sie Folgendes:

  • Erstellen eines Warteschlangendiensts.
  • Delegieren der Arbeit an eine Aufgabenwarteschlange.
  • Registrieren eines Konsolentastenlisteners von IHostApplicationLifetime-Ereignissen aus.

Tipp

Der gesamte Quellcode des Beispiels „Worker in .NET“ steht im Beispielbrowser zum Download zur Verfügung. Weitere Informationen finden Sie unter Durchsuchen von Codebeispielen: Worker in .NET.

Voraussetzungen

Erstellen eines neuen Projekts

Um ein neues Workerdienstprojekt mit Visual Studio zu erstellen, wählen Sie Datei>Neu>Projekt... aus. Suchen Sie im Dialogfeld Neues Projekt erstellen nach „Workerdienst“, und wählen Sie die Workerdienstvorlage aus. Wenn Sie lieber die .NET-CLI verwenden möchten, öffnen Sie Ihr bevorzugtes Terminal in einem Arbeitsverzeichnis. Führen Sie den Befehl dotnet new aus, und ersetzen Sie <Project.Name> durch den gewünschten Projektnamen.

dotnet new worker --name <Project.Name>

Weitere Informationen zum .NET-CLI-Befehl für ein neues Workerdienstprojekt finden Sie unter dotnet new worker.

Tipp

Wenn Sie Visual Studio Code verwenden, können Sie .NET CLI-Befehle über das integrierte Terminal ausführen. Weitere Informationen finden Sie unter Visual Studio Code: Integriertes Terminal.

Erstellen von Warteschlangendiensten

Möglicherweise sind Sie mit den QueueBackgroundWorkItem(Func<CancellationToken,Task>)-Funktionen aus dem System.Web.Hosting-Namespace vertraut.

Tipp

Die Funktionalität des System.Web-Namespace wurde absichtlich nicht zu .NET portiert und kann ausschließlich in .NET Framework verwendet werden. Weitere Informationen finden Sie unter Erste Schritte mit der inkrementellen Migration von ASP.NET zu ASP.NET Core.

Um in .NET einen an der Funktionalität von QueueBackgroundWorkItem orientierten Dienst zu modellieren, fügen Sie dem Projekt zunächst eine IBackgroundTaskQueue-Schnittstelle hinzu:

namespace App.QueueService;

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

Es gibt zwei Methoden: eine, die Warteschlangenfunktionen verfügbar macht, und eine andere, die zuvor in die Warteschlange eingereihte Arbeitselemente aus der Warteschlange entfernt. Ein Arbeitselement ist eine Func<CancellationToken, ValueTask>-Klasse. Fügen Sie als Nächstes dem Projekt die Standardimplementierung hinzu.

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

Die vorherige Implementierung basiert auf einem Channel<T> als Warteschlange. Die BoundedChannelOptions(Int32) wird mit einer expliziten Kapazität aufgerufen. Die Kapazität sollte basierend auf der erwarteten Anwendungslast und der Anzahl gleichzeitiger Threads festgelegt werden, die auf die Warteschlange zugreifen. BoundedChannelFullMode.Wait führt dazu, dass Aufrufe von ChannelWriter<T>.WriteAsync eine Aufgabe zurückgeben, die nur abgeschlossen wird, wenn Speicherplatz verfügbar wird. Dies führt zu einem Rückstau, falls sich zu viele Herausgeber/Aufrufe anhäufen.

Erneutes Schreiben der Worker-Klasse

Im folgenden Beispiel für QueueHostedService gilt:

  • Die ProcessTaskQueueAsync-Methode gibt einen Task in ExecuteAsync zurück.
  • Hintergrundtasks in der Warteschlange werden aus dieser entfernt und in ProcessTaskQueueAsync ausgeführt.
  • Auf Arbeitselemente wird gewartet, bevor der Dienst in StopAsync angehalten wird.

Ersetzen Sie die vorhandene Worker-Klasse durch den folgenden C#-Code, und benennen Sie die Datei in QueueHostedService.cs um.

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);
    }
}

Ein MonitorLoop-Dienst verarbeitet das Einreihen von Tasks in die Warteschlange für den gehosteten Dienst, wenn der w-Schlüssel auf einem Eingabegerät ausgewählt wird:

  • Die IBackgroundTaskQueue wird in den MonitorLoop-Dienst eingefügt.
  • IBackgroundTaskQueue.QueueBackgroundWorkItemAsync wird aufgerufen, um ein Arbeitselement in die Warteschlange einzureihen.
  • Das Arbeitselement simuliert eine Hintergrundaufgabe mit langer Ausführungszeit:
    • Drei 5-Sekunden-Verzögerungen werden ausgeführt (Delay).
    • Eine try-catch-Anweisung fängt OperationCanceledException auf, wenn der Task abgebrochen wird.
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);
        }
    }
}

Ersetzen Sie den vorhandenen Inhalt von Program durch den folgenden C#-Code:

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();

Die Dienste werden in (Program.cs) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService registriert. MonitorLoop wird in der Program.cs-Anweisung der obersten Ebene gestartet:

MonitorLoop monitorLoop = host.Services.GetRequiredService<MonitorLoop>()!;
monitorLoop.StartMonitorLoop();

Weitere Informationen zum Registrieren von Diensten finden Sie unter Abhängigkeitsinjektion in .NET.

Überprüfen der Dienstfunktionalität

Zum Ausführen der Anwendung aus Visual Studio drücken Sie F5, oder wählen Sie die Menüoption Debuggen>Debuggen starten aus. Wenn Sie die .NET-CLI verwenden, führen Sie den Befehl dotnet run im Arbeitsverzeichnis aus:

dotnet run

Weitere Informationen zum Ausführungsbefehl in der .NET-CLI finden Sie unter dotnet run.

Wenn Sie dazu aufgefordert werden, geben Sie w (oder W) mindestens einmal ein, um ein emuliertes Arbeitselement in die Warteschlange zu stellen, wie in der Beispielausgabe gezeigt:

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.

Wenn Sie die Anwendung in Visual Studio ausführen, wählen Sie Debuggen>Debuggen beenden... aus. Alternativ können Sie im Konsolenfenster STRG + C drücken, um den Abbruch zu signalisieren.

Siehe auch