Partilhar via


Criar um serviço de fila

Um serviço de fila é um ótimo exemplo de um serviço de longa execução, onde os itens de trabalho podem ser enfileirados e trabalhados sequencialmente à medida que os itens de trabalho anteriores são concluídos. Baseando-se no modelo de Serviço de Trabalho, você cria novas funcionalidades sobre o BackgroundService.

Neste tutorial, irá aprender a:

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

Gorjeta

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

Pré-requisitos

Criar um novo projeto

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

dotnet new worker --name <Project.Name>

Para obter mais informações sobre o comando .NET CLI new worker service project, consulte dotnet new worker.

Gorjeta

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

Criar serviços de fila

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

Gorjeta

A funcionalidade do namespace não foi intencionalmente transferida para o System.Web .NET e permanece exclusiva do .NET Framework. Para obter mais informações, consulte Introdução ao ASP.NET incremental para migração 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 expõe a funcionalidade de enfileiramento e outro que desfila itens de trabalho enfileirados anteriormente. Um item de trabalho é um Func<CancellationToken, ValueTask>arquivo . 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 esperada do aplicativo e no número de threads simultâneos que acessam a fila. BoundedChannelFullMode.Wait faz com que as chamadas retornem ChannelWriter<T>.WriteAsync uma tarefa, que é concluída somente quando o espaço fica disponível. O que leva a uma pressão negativa, no caso de muitos editores/chamadas começarem a acumular-se.

Reescrever a classe Worker

No exemplo a seguir QueueHostedService :

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

Substitua a classe existente Worker pelo seguinte código C# 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 MonitorLoop serviço lida com tarefas de enfileiramento para o serviço hospedado sempre que a w chave é selecionada em um dispositivo de entrada:

  • O IBackgroundTaskQueue é injetado MonitorLoop no serviço.
  • IBackgroundTaskQueue.QueueBackgroundWorkItemAsync é chamado para enfileirar um item de trabalho.
  • O item de trabalho simula uma tarefa em segundo plano de longa duração:
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 estão registados em (Programa.cs). O serviço hospedado é registrado com o AddHostedService método de extensão. MonitorLoop é iniciado na instrução de nível superior .cs Programa:

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

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

Verificar a funcionalidade do serviço

Para executar o aplicativo do Visual Studio, selecione F5 ou selecione a opção de menu Depurar 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 .NET CLI run, consulte dotnet run.

Quando solicitado, digite 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 de dentro do Visual Studio, selecione Depurar Parar Depuração>.... Como alternativa, selecione Ctrl + C na janela do console para sinalizar o cancelamento.

Consulte também