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 las tareas pueden encolarse y procesarse de manera secuencial a medida que se completan las anteriores. Basándose en la plantilla Worker Service, se desarrolla una nueva funcionalidad sobre el BackgroundService.

En este tutorial, aprenderá a:

  • Cree un servicio de cola.
  • Delegar el trabajo en una cola de tareas.
  • Registre un receptor de teclas de la consola desde IHostApplicationLifetime eventos.

Sugerencia

Todo el código fuente de ejemplo de "Workers in .NET" está disponible en el Explorador de Muestras para su descarga. Para obtener más información, vea Examinar ejemplos de código: Trabajos en .NET.

Prerrequisitos

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 el <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 obtener más información, vea Visual Studio Code: Terminal integrado.

Creación de servicios de puesta en cola

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

Sugerencia

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

En .NET, para modelar un servicio inspirado en la QueueBackgroundWorkItem funcionalidad, empiece agregando una IBackgroundTaskQueue interfaz 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 desencola elementos de trabajo que habían sido puestos en cola. Un elemento de trabajo es un 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 configurarse según la carga de aplicación esperada y el número de hilos simultáneos que acceden a la cola. BoundedChannelFullMode.Wait hace que las llamadas a ChannelWriter<T>.WriteAsync devuelvan una tarea, que solo se completa cuando el espacio está disponible. Lo que conduce a la contrapresión, en caso de que demasiados publicadores o llamadas empiecen a acumularse.

Reescribir la clase Worker

En el ejemplo QueueHostedService siguiente:

  • El ProcessTaskQueueAsync método devuelve un 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 Worker por el siguiente código de C# y cambie el nombre del archivo 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 existente 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 alojado se registra con el método de extensión AddHostedService. MonitorLoop se inicia en la instrucción principal de 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 seleccione la opción de menú Iniciar depuración>. Si usa la CLI de .NET, ejecute el dotnet run comando 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 solicite, 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 Visual Studio, seleccione Depurar>.... Como alternativa, seleccione Ctrl + C en la ventana de la consola para indicar la cancelación.

Consulte también