Share via


Creare un servizio di accodamento

Un servizio di accodamento è un ottimo esempio di servizio a esecuzione prolungata, in cui gli elementi di lavoro possono essere accodati e usati in sequenza a mano a mano che vengono completati gli elementi di lavoro precedenti. Facendo affidamento sul modello Servizio ruolo di lavoro, si creano nuove funzionalità oltre a BackgroundService.

In questa esercitazione apprenderai a:

  • Creare un servizio di accodamento.
  • Delegare il lavoro a una coda di attività.
  • Registrare un listener chiave della console dagli eventi IHostApplicationLifetime.

Suggerimento

Tutto il codice sorgente di esempio di "Servizi ruolo di lavoro in .NET" è disponibile per il download in Esplorazione campioni. Per altre informazioni, vedere Esplorare esempi di codice: Servizi ruolo di lavoro in .NET.

Prerequisiti

Crea un nuovo progetto

Per creare un nuovo progetto del Servizio ruolo di lavoro con Visual Studio, selezionare File>Nuovo>Progetto.... Nella finestra di dialogo Crea un nuovo progetto cercare "Servizio del ruolo di lavoro" e selezionare il modello Servizio ruolo di lavoro. Se si preferisce usare l'interfaccia della riga di comando di .NET, aprire il terminale preferito in una directory di lavoro. Eseguire il comando dotnet new e sostituire <Project.Name> con il nome del progetto desiderato.

dotnet new worker --name <Project.Name>

Per altre informazioni sul comando new worker service project dell'interfaccia della riga di comando di .NET, vedere dotnet new worker.

Suggerimento

Se si usa Visual Studio Code, è possibile eseguire i comandi dell'interfaccia della riga di comando di .NET dal terminale integrato. Per altre informazioni, vedere Visual Studio Code: terminale integrato.

Creare servizi di accodamento

È possibile avere familiarità con la funzionalità QueueBackgroundWorkItem(Func<CancellationToken,Task>) dello spazio dei nomi System.Web.Hosting.

Suggerimento

Le funzionalità dello spazio dei nomi System.Web non sono state intenzionalmente convertite in .NET e rimangono esclusive di .NET Framework. Per altre informazioni, vedere Introduzione alla migrazione incrementale da ASP.NET ad ASP.NET Core.

In .NET, per modellare un servizio ispirato alla funzionalità QueueBackgroundWorkItem, iniziare aggiungendo un'interfaccia IBackgroundTaskQueue al progetto:

namespace App.QueueService;

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

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

Esistono due metodi, uno che espone la funzionalità di accodamento e un altro che rimuove dalla coda gli elementi di lavoro accodati in precedenza. Un elemento di lavoro è Func<CancellationToken, ValueTask>. Aggiungere quindi l'implementazione predefinita al progetto.

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

L'implementazione precedente si basa su Channel<T> come coda. Viene effettuata la chiamata a BoundedChannelOptions(Int32) con una capacità esplicita. La capacità deve essere impostata in base al carico previsto dell'applicazione e al numero di thread simultanei che accedono alla coda. BoundedChannelFullMode.Wait fa sì che le chiamate a ChannelWriter<T>.WriteAsync restituiscano un'attività, che viene completata solo quando lo spazio diventa disponibile. Ciò porta alla backpressure, nel caso in cui troppi server di pubblicazione/chiamate inizino ad accumularsi.

Riscrivere la classe Ruolo di lavoro

Nell'esempio seguente QueueHostedService:

  • Il metodo ProcessTaskQueueAsync restituisce un'istanza Task in ExecuteAsync.
  • Le attività in background nella coda vengono rimosse dalla coda ed eseguite in ProcessTaskQueueAsync.
  • Gli elementi di lavoro sono attesi prima che il servizio si arresti in StopAsync.

Sostituire la classe Worker esistente con il codice C# seguente e rinominare il file 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);
    }
}

Un servizio MonitorLoop gestisce le attività di accodamento per il servizio ospitato ogni volta in cui la chiave w viene selezionata in un dispositivo di input:

  • Viene effettuato l'inserimento di IBackgroundTaskQueue nel servizio MonitorLoop.
  • Viene effettuata la chiamata di IBackgroundTaskQueue.QueueBackgroundWorkItemAsync per accodare un elemento di lavoro.
  • L'elemento di lavoro simula un'attività in background a esecuzione prolungata:
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);
        }
    }
}

Sostituire i contenuti esistenti Program con il codice C# seguente:

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

I servizi vengono registrati in (Program.cs). Il servizio ospitato viene registrato con il metodo di estensione AddHostedService. Viene eseguito l'avvio di MonitorLoop nell'istruzione Program.cs di primo livello:

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

Per altre informazioni sulla registrazione dei servizi, vedere Inserimento delle dipendenze in .NET.

Verificare la funzionalità del servizio

Per eseguire l'applicazione da Visual Studio, selezionare F5 oppure selezionare l'opzione di menu Debug>Avvia debug. Se si usa l'interfaccia della riga di comando di .NET, eseguire il comando dotnet run dalla directory di lavoro:

dotnet run

Per altre informazioni sul comando di esecuzione dell'interfaccia della riga di comando di .NET, vedere dotnet run.

Quando viene richiesto di immettere w (o W) almeno una volta per accodare un elemento di lavoro emulato, come illustrato nell'output di esempio:

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.

Se si esegue l'applicazione da Visual Studio, selezionare Debug>Arresta debug.... In alternativa, selezionare CTRL + C dalla finestra della console per segnalare l'annullamento.

Vedi anche