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
- O SDK do .NET 8.0 ou posterior
- Um ambiente de desenvolvimento integrado do .NET (IDE)
- Fique à vontade para usar o Visual Studio
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 emExecuteAsync
. - 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çoMonitorLoop
. - O
IBackgroundTaskQueue.QueueBackgroundWorkItemAsync
é chamado para enfileirar um item de trabalho. - O item de trabalho simula uma tarefa em segundo plano de execução longa:
- Três atrasos de 5 segundos são executados Delay.
- Uma instrução
try-catch
intercepta OperationCanceledException se a tarefa for cancelada.
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.