Compartilhar via


Criar um serviço de fila

Um serviço de fila é um ótimo exemplo de um serviço de longa duração, em que os itens de trabalho podem ser enfileirados e processados sequencialmente à medida que os itens de trabalho anteriores são concluídos. Dependendo do modelo do Serviço de Trabalho, você cria uma nova funcionalidade na parte superior do BackgroundService.

Neste tutorial, você aprenderá como:

  • Crie um serviço de fila.
  • Delegar trabalho para uma fila de tarefas.
  • Registre um ouvinte de teclas do console para eventos de IHostApplicationLifetime.

Dica

Todo o código-fonte de exemplo "Trabalhadores no .NET" está disponível no Navegador de Exemplos para download. Para obter mais informações, consulte Procurar exemplos de código: Trabalhos no .NET.

Pré-requisitos

Criar um novo projeto

Para criar um projeto do Serviço de Trabalho com o Visual Studio, selecione Arquivo>Novo>Projeto.... Na caixa de diálogo Criar um projeto, pesquise "Serviço de Trabalho" e selecione o modelo de Serviço de Trabalho. Se você preferir usar a CLI do .NET, abra seu terminal favorito em um diretório de trabalho. Execute o comando dotnet new e substitua o <Project.Name> pelo nome do projeto desejado.

dotnet new worker --name <Project.Name>

Para obter mais informações sobre o novo comando do projeto de serviço de trabalho da CLI do .NET, confira dotnet new worker.

Dica

Se você estiver usando o Visual Studio Code, poderá executar comandos da CLI do .NET no terminal integrado. Para obter mais informações, consulte Visual Studio Code: Terminal Integrado.

Criar serviços de enfileiramento

Você pode estar familiarizado com a QueueBackgroundWorkItem(Func<CancellationToken,Task>) funcionalidade do System.Web.Hosting namespace.

Dica

A funcionalidade do System.Web namespace não foi portada intencionalmente para o .NET e permanece exclusiva para o .NET Framework. Para obter mais informações, consulte Introdução à migração incremental de ASP.NET para ASP.NET Core.

No .NET, para modelar um serviço inspirado na QueueBackgroundWorkItem funcionalidade, comece adicionando uma IBackgroundTaskQueue interface ao projeto:

namespace App.QueueService;

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

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

Há dois métodos, um que oferece a funcionalidade de enfileiramento e outro que desenfileira itens de trabalho previamente enfileirados. Um item de trabalho é um Func<CancellationToken, ValueTask>. Em seguida, adicione a implementação padrão ao projeto.

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

A implementação anterior depende de um Channel<T> como uma fila. O BoundedChannelOptions(Int32) é chamado com uma capacidade explícita. A capacidade deve ser definida com base na carga de aplicativo esperada e no número de threads simultâneos que acessam a fila. BoundedChannelFullMode.Wait faz com que as chamadas ChannelWriter<T>.WriteAsync retornem uma tarefa, que é concluída somente quando o espaço fica disponível. O que leva à contrapressão, caso muitos publicadores/chamadas comecem a se acumular.

Reescrever a classe Worker

No exemplo QueueHostedService a seguir:

  • O ProcessTaskQueueAsync método retorna um Task em ExecuteAsync.
  • As tarefas em segundo plano na fila são removidas da fila e executadas em ProcessTaskQueueAsync.
  • Os itens de trabalho são aguardados antes que o serviço pare em StopAsync.

Substitua a classe existente Worker pelo código C# a seguir e renomeie o arquivo para 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);
    }
}

Um serviço MonitorLoop lida com tarefas de enfileiramento para o serviço hospedado sempre que a chave w é selecionada em um dispositivo de entrada:

  • A IBackgroundTaskQueue é injetada no serviço MonitorLoop.
  • O IBackgroundTaskQueue.QueueBackgroundWorkItemAsync é chamado para enfileirar um item de trabalho.
  • O item de trabalho simula uma tarefa em segundo plano de execução longa:
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);
        }
    }
}

Substitua o conteúdo existente Program pelo seguinte código 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();

Os serviços são registrados em (Program.cs). O serviço hospedado é registrado com o método AddHostedService de extensão. MonitorLoop é iniciado em Program.cs instrução de nível superior:

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

Para obter mais informações para registrar serviços, confira Injeção de dependência no .NET.

Verificar a funcionalidade do serviço

Para executar o aplicativo no Visual Studio, selecione F5 ou selecione a opção de menu Depuração>Iniciar Depuração. Se você estiver usando a CLI do .NET, execute o dotnet run comando no diretório de trabalho:

dotnet run

Para obter mais informações sobre o comando run da CLI do .NET, consulte dotnet run.

Quando solicitado, insira o w (ou W) pelo menos uma vez para enfileirar um item de trabalho emulado, conforme mostrado na saída de exemplo:

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 estiver executando o aplicativo no Visual Studio, selecione Depurar>Parar Depuração.... Como alternativa, selecione Ctrl + C na janela do console para sinalizar o cancelamento.

Consulte também