Criar um serviço Fila

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

Neste tutorial, você aprenderá a:

  • Criar um serviço fila.
  • Delegar trabalho para uma fila de tarefas.
  • Registre um ouvinte de chave de console a partir de eventos IHostApplicationLifetime.

Dica

Todo o código-fonte do exemplo dos “Trabalhos no .NET” está disponível no Navegador de Exemplos para download. Para obter mais informações, confira 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 preferir usar a CLI do .NET, abra seu terminal favorito em um diretório de trabalho. Execute o comando dotnet new e substitua <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, confira Visual Studio Code: Terminal Integrado.

Criar serviços de enfileiramento

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

Dica

A funcionalidade do namespace System.Web 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 do ASP.NET para o ASP.NET Core.

No .NET, para modelar um serviço inspirado pela funcionalidade QueueBackgroundWorkItem, comece adicionando uma interface IBackgroundTaskQueue 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 expõe a funcionalidade de enfileiramento e outro que remove da fila itens de trabalho previamente enfileirados. Um item de trabalho é uma 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. Isso leva a pressão reversa caso muitos editores/chamadas começarem a se acumular.

Reescrever a classe Worker

No exemplo QueueHostedService a seguir:

  • O método ProcessTaskQueueAsync retorna uma 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 seguinte código C# e renomeie o arquivo para ScopedBackgroundService.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 Program existente 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 está registrado com o método de extensão AddHostedService. MonitorLoop é iniciado na instrução de nível superior Program.cs:

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 a opção do menu Depuração>Iniciar Depuração. Se você estiver usando a CLI do .NET, execute o comando dotnet run no diretório de trabalho:

dotnet run

Para obter mais informações sobre o comando de execução da CLI do .NET, confira a execução do dotnet.

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.

Confira também