Compartir a través de


Creación de un servicio de cola

Un servicio de cola es un excelente ejemplo de un servicio de larga duración, donde los elementos de trabajo se pueden poner en cola y trabajar secuencialmente a medida que se completan los elementos de trabajo anteriores. En función de la plantilla de servicio de trabajo, se compilará una nueva funcionalidad, además de BackgroundService.

En este tutorial aprenderá a:

  • Crear un servicio de cola.
  • Delegar el trabajo en una cola de tareas.
  • Registrar un agente de escucha de claves de consola a partir de eventos IHostApplicationLifetime.

Sugerencia

Todo el ejemplo de "Trabajos en .NET" está disponible en el Explorador de ejemplos para su descarga. Para obtener más información, consulte Examinación de ejemplos de código: Trabajos en .NET.

Requisitos previos

Creación de un nuevo proyecto

Para crear un proyecto de Worker Service con Visual Studio, seleccione Archivo>Nuevo>Proyecto... . En el cuadro de diálogo Crear un proyecto, busque "Worker Service" y seleccione la plantilla Worker Service. Si prefiere usar la CLI de .NET, abra su terminal favorito en un directorio de trabajo. Ejecute el comando dotnet new y reemplace <Project.Name> por el nombre del proyecto deseado.

dotnet new worker --name <Project.Name>

Para más información sobre el comando del nuevo proyecto de Worker Service de la CLI de .NET, vea dotnet new worker.

Sugerencia

Si usa Visual Studio Code, puede ejecutar comandos de la CLI de .NET desde el terminal integrado. Para más información, vea Visual Studio Code: terminal integrado.

Creación de servicios de cola

Es posible que esté familiarizado con la funcionalidad QueueBackgroundWorkItem(Func<CancellationToken,Task>) del espacio de nombres System.Web.Hosting.

Sugerencia

La funcionalidad del espacio de nombres System.Web no se ha pasado intencionadamente a .NET y sigue siendo exclusiva de .NET Framework. Para obtener más información, consulte Introducción a la migración incremental de ASP.NET a ASP.NET Core.

En .NET, para modelar un servicio que se inspira la funcionalidad QueueBackgroundWorkItem, empiece por agregar una interfaz IBackgroundTaskQueue al proyecto:

namespace App.QueueService;

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

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

Hay dos métodos, uno que expone la funcionalidad de puesta en cola y otro que quita de la cola los elementos de trabajo previamente en cola. Un elemento de trabajo es Func<CancellationToken, ValueTask>. A continuación, agregue la implementación predeterminada al proyecto.

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

La implementación anterior se basa en Channel<T> como una cola. Se llama a BoundedChannelOptions(Int32) con una capacidad explícita. La capacidad debe establecerse en función de la carga esperada de la aplicación y el número de subprocesos simultáneos que acceden a la cola. BoundedChannelFullMode.Wait hace que las llamadas a ChannelWriter<T>.WriteAsync devuelvan una tarea, que se completa solo cuando el espacio está disponible. Esto genera contrapresión, en caso de que se empiecen a acumular demasiados publicadores o llamadas.

Reescritura de la clase Worker

En el ejemplo QueueHostedService siguiente:

  • El método ProcessTaskQueueAsync devuelve Task en ExecuteAsync.
  • Las tareas en segundo plano que están en cola se quitan de ella y se ejecutan en ProcessTaskQueueAsync.
  • Se esperan elementos de trabajo antes de que el servicio se detenga en StopAsync.

Reemplace la clase existente por el siguiente código de C# y cambie el nombre del archivo Worker a 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 servicio MonitorLoop controla las tareas de puesta en cola para el servicio hospedado cada vez que se selecciona la tecla w en un dispositivo de entrada:

  • IBackgroundTaskQueue se inserta en el servicio MonitorLoop.
  • Se llama a IBackgroundTaskQueue.QueueBackgroundWorkItemAsync para poner en cola el elemento de trabajo.
  • El elemento de trabajo simula una tarea en segundo plano de larga duración:
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);
        }
    }
}

Reemplace el contenido Program por el siguiente código de 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();

Los servicios se registran en (Program.cs). El servicio hospedado se registra con el método de extensión AddHostedService. MonitorLoop se inicia en la instrucción de nivel superior Program.cs:

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

Para obtener más información sobre el registro de servicios, consulte Inserción de dependencias en .NET.

Comprobación de la funcionalidad del servicio

Para ejecutar la aplicación desde Visual Studio, seleccione F5 o la opción de menú Depurar>Iniciar depuración. Si usa la CLI de .NET, ejecute el comando dotnet run desde el directorio de trabajo:

dotnet run

Para más información sobre el comando run de la CLI de .NET, consulte dotnet run.

Cuando se le pida que escriba w (o W) al menos una vez para poner en cola un elemento de trabajo emulado, como se muestra en la salida de ejemplo:

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.

Si ejecuta la aplicación desde dentro Visual Studio, seleccione Depurar>Detener depuración... . Como alternativa, seleccione Ctrl + C en la ventana de consola para indicar la cancelación.

Vea también