Een wachtrijservice maken

Een wachtrijservice is een goed voorbeeld van een langlopende service, waarbij werkitems in de wachtrij kunnen worden geplaatst en sequentieel kunnen worden gewerkt wanneer eerdere werkitems zijn voltooid. Als u afhankelijk bent van de worker-servicesjabloon, bouwt u nieuwe functionaliteit op boven op de BackgroundServicesjabloon.

In deze zelfstudie leert u het volgende:

  • Maak een wachtrijservice.
  • Werk delegeren aan een taakwachtrij.
  • Registreer een consolesleutellistener van IHostApplicationLifetime gebeurtenissen.

Tip

Alle voorbeeldbroncode 'Workers in .NET' is beschikbaar in de voorbeeldenbrowser om te downloaden. Zie Codevoorbeelden bekijken: Workers in .NET voor meer informatie.

Vereisten

  • De .NET 8.0 SDK of hoger
  • Een .NET Integrated Development Environment (IDE)
    • U kunt Visual Studio gerust gebruiken

Een nieuw project maken

Als u een nieuw Worker Service-project wilt maken met Visual Studio, selecteert u Bestand>nieuw>project.... Zoek in het dialoogvenster Een nieuw project maken naar 'Worker Service' en selecteer de sjabloon Worker Service. Als u liever de .NET CLI gebruikt, opent u uw favoriete terminal in een werkmap. Voer de dotnet new opdracht uit en vervang de door de <Project.Name> gewenste projectnaam.

dotnet new worker --name <Project.Name>

Zie dotnet new worker voor meer informatie over de opdracht .NET CLI new worker service project.

Tip

Als u Visual Studio Code gebruikt, kunt u .NET CLI-opdrachten uitvoeren vanuit de geïntegreerde terminal. Zie Visual Studio Code: Integrated Terminal voor meer informatie.

Wachtrijservices maken

Mogelijk bent u bekend met de QueueBackgroundWorkItem(Func<CancellationToken,Task>) functionaliteit van de System.Web.Hosting naamruimte.

Tip

De functionaliteit van de System.Web naamruimte is opzettelijk niet overgedragen naar .NET en blijft exclusief voor .NET Framework. Zie Aan de slag met incrementele ASP.NET voor ASP.NET Core-migratie voor meer informatie.

Als u in .NET een service wilt modelleren die is geïnspireerd op de QueueBackgroundWorkItem functionaliteit, begint u met het toevoegen van een IBackgroundTaskQueue interface aan het project:

namespace App.QueueService;

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

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

Er zijn twee methoden, een die wachtrijfunctionaliteit beschikbaar maakt en een andere methode die eerder in de wachtrij geplaatste werkitems dequenteert. Een werkitem is een Func<CancellationToken, ValueTask>. Voeg vervolgens de standaard implementatie toe aan het project.

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

De voorgaande implementatie is afhankelijk van een Channel<T> wachtrij. De BoundedChannelOptions(Int32) naam wordt aangeroepen met een expliciete capaciteit. Capaciteit moet worden ingesteld op basis van de verwachte belasting van de toepassing en het aantal gelijktijdige threads die toegang hebben tot de wachtrij. BoundedChannelFullMode.Wait zorgt ervoor dat aanroepen ChannelWriter<T>.WriteAsync een taak retourneren, die alleen wordt voltooid wanneer er ruimte beschikbaar is. Wat leidt tot backpressie, voor het geval er te veel uitgevers/aanroepen beginnen te accumuleren.

De werkrolklasse herschrijven

In het volgende QueueHostedService voorbeeld:

  • De ProcessTaskQueueAsync methode retourneert een Task in ExecuteAsync.
  • Achtergrondtaken in de wachtrij worden ontkend en uitgevoerd in ProcessTaskQueueAsync.
  • Werkitems worden gewacht voordat de service stopt.StopAsync

Vervang de bestaande Worker klasse door de volgende C#-code en wijzig de naam van het bestand in 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);
    }
}

Een MonitorLoop service verwerkt enqueuingtaken voor de gehoste service wanneer de w sleutel wordt geselecteerd op een invoerapparaat:

  • De IBackgroundTaskQueue wordt in de MonitorLoop service geïnjecteerd.
  • IBackgroundTaskQueue.QueueBackgroundWorkItemAsync wordt aangeroepen om een werkitem in de wachtrij te zetten.
  • Het werkitem simuleert een langlopende achtergrondtaak:
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);
        }
    }
}

Vervang de bestaande Program inhoud door de volgende 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();

De services worden geregistreerd in (Program.cs). De gehoste service wordt geregistreerd bij de AddHostedService extensiemethode. MonitorLoop is gestart in de instructie op het hoogste niveau van Program.cs :

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

Zie Afhankelijkheidsinjectie in .NET voor meer informatie over het registreren van services.

Servicefunctionaliteit controleren

Als u de toepassing vanuit Visual Studio wilt uitvoeren, selecteert u F5 of selecteert u de menuoptie Foutopsporing> starten. Als u de .NET CLI gebruikt, voert u de dotnet run opdracht uit vanuit de werkmap:

dotnet run

Zie dotnet run voor meer informatie over de .NET CLI-run-opdracht.

Wanneer u wordt gevraagd het (of W) minstens één keer in te voeren om een geëmuleerd werkitem in de w wachtrij te plaatsen, zoals wordt weergegeven in de voorbeelduitvoer:

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.

Als u de toepassing vanuit Visual Studio uitvoert, selecteert u Foutopsporing>stoppen.... U kunt ook Ctrl + C selecteren in het consolevenster om annulering te signaleren.

Zie ook