Tarefas em segundo plano com serviços hospedados no ASP.NET Core

Por Jeow Li Huan

No ASP.NET Core, as tarefas em segundo plano podem ser implementadas como serviços hospedados. Um serviço hospedado é uma classe com lógica de tarefa em segundo plano que implementa a interface IHostedService. Este artigo fornece três exemplos de serviço hospedado:

  • Tarefa em segundo plano que é executada com um temporizador.
  • Serviço hospedado que ativa um serviço com escopo. O serviço com escopo pode usar DI (injeção de dependência).
  • Tarefas em segundo plano na fila que são executadas sequencialmente.

Modelo de serviço de trabalho

O modelo de Serviço de Trabalho do ASP.NET Core fornece um ponto inicial para escrever aplicativos de serviço de execução prolongada. Um aplicativo criado a partir do modelo do Serviço de Trabalho especifica o SDK do Worker em seu arquivo de projeto:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Para usar o modelo como base para um aplicativo de serviços hospedados:

  1. Criar um novo projeto.
  2. Selecione Serviço de Trabalho. Selecione Avançar.
  3. Forneça um nome ao projeto no campo Nome do projeto ou aceite o nome do projeto padrão. Selecione Avançar.
  4. Na caixa de diálogo Informações adicionais , escolha uma estrutura. Selecione Criar.

Pacote

Um aplicativo baseado no modelo do Serviço de Trabalho usa o Microsoft.NET.Sdk.Worker SDK e tem uma referência explícita de pacote para o pacote Microsoft.Extensions.Hosting . Por exemplo, consulte o arquivo de projeto do aplicativo de exemplo (BackgroundTasksSample.csproj).

Para aplicativos Web que usam o Microsoft.NET.Sdk.Web SDK, o pacote Microsoft.Extensions.Hosting é referenciado implicitamente da estrutura compartilhada. Não é necessária uma referência explícita de pacote no arquivo de projeto do aplicativo.

Interface IHostedService

A IHostedService interface define dois métodos para objetos gerenciados pelo host:

StartAsync

StartAsync(CancellationToken) contém a lógica para iniciar a tarefa em segundo plano. StartAsync é chamado antes:

StartAsync deve ser limitado a tarefas de execução curta porque os serviços hospedados são executados sequencialmente e nenhum serviço adicional é iniciado até que sejam executados até StartAsync a conclusão.

StopAsync

O token de cancelamento tem um tempo limite padrão de cinco segundos para indicar que o processo de desligamento não deve mais ser normal. Quando for solicitado um cancelamento no token:

  • Todas as demais operações em segundo plano que o aplicativo estiver executando deverão ser anuladas.
  • Todos os métodos chamados em StopAsync deverão retornar imediatamente.

No entanto, as tarefas não são abandonadas após a solicitação de cancelamento — o chamador aguarda a conclusão de todas as tarefas.

Se o aplicativo for desligado inesperadamente (por exemplo, em uma falha do processo do aplicativo), StopAsync não poderá ser chamado. Portanto, os métodos chamados ou operações realizadas em StopAsync talvez não ocorram.

Para estender o tempo limite de desligamento padrão de cinco segundos, defina:

O serviço hospedado é ativado uma única vez na inicialização do aplicativo e desligado normalmente durante o desligamento do aplicativo. Se um erro for gerado durante a execução da tarefa em segundo plano, Dispose deverá ser chamado mesmo se StopAsync não for chamado.

Classe base BackgroundService

BackgroundService é uma classe base para implementar uma execução longa IHostedService.

ExecuteAsync(CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna um Task que representa todo o tempo de vida do serviço em segundo plano. Nenhum serviço adicional é iniciado até que ExecuteAsync se torne assíncrono, como chamando await. Evite executar trabalhos longos e de bloqueio de inicialização em ExecuteAsync. Os blocos de host em StopAsync(CancellationToken) aguardando ExecuteAsync a conclusão.

O token de cancelamento é disparado quando IHostedService.StopAsync é chamado. Sua implementação ExecuteAsync deve ser concluída prontamente quando o token de cancelamento for acionado para desligar o serviço normalmente. Caso contrário, o serviço é desligado sem êxito no tempo limite de desligamento. Para obter mais informações, consulte a seção de interface IHostedService .

Para obter mais informações, consulte o código-fonte BackgroundService .

Tarefas em segundo plano temporizadas

Uma tarefa em segundo plano temporizada usa a classe System.Threading.Timer. O temporizador dispara o método DoWork da tarefa. O temporizador é desabilitado em StopAsync e descartado quando o contêiner de serviço é descartado em Dispose:

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer = null;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

O Timer não aguarda a conclusão das DoWork execuções anteriores, portanto, a abordagem mostrada pode não ser adequada para todos os cenários. Interlocked.Increment é usado para incrementar o contador de execução como uma operação atômica, o que garante que vários threads não sejam atualizados executionCount simultaneamente.

O serviço está registrado em IHostBuilder.ConfigureServices (Program.cs) com o AddHostedService método de extensão:

services.AddHostedService<TimedHostedService>();

Consumindo um serviço com escopo em uma tarefa em segundo plano

Para usar serviços com escopo em um BackgroundService, crie um escopo. Por padrão, nenhum escopo é criado para um serviço hospedado.

O serviço da tarefa em segundo plano com escopo contém a lógica da tarefa em segundo plano. No exemplo a seguir:

  • O serviço é assíncrono. O método DoWork retorna uma Task. Para fins de demonstração, um atraso de dez segundos é aguardado no DoWork método.
  • Um ILogger é injetado no serviço.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

O serviço hospedado cria um escopo para resolver o serviço de tarefa em segundo plano com escopo para chamar seu DoWork método. DoWork retorna um Task, que é aguardado em ExecuteAsync:

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Os serviços estão registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registrado com o método de AddHostedService extensão:

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Tarefas em segundo plano na fila

Uma fila de tarefas em segundo plano é baseada no .NET 4.x QueueBackgroundWorkItem:

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

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

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

No exemplo a seguir QueueHostedService :

  • O BackgroundProcessing método retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são dequeu e executadas em BackgroundProcessing.
  • Os itens de trabalho são aguardados antes que o serviço pare.StopAsync
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service 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:

  • Ele IBackgroundTaskQueue é injetado no MonitorLoop serviço.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem é 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 (Task.Delay).
    • Uma try-catch instrução será interceptada OperationCanceledException se a tarefa for cancelada.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue,
        ILogger<MonitorLoop> logger,
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("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(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {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 Background Task {Guid} is running. " 
                                   + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

Os serviços estão registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registrado com o método de AddHostedService extensão:

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop é iniciado em Program.cs:

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

Recursos adicionais

No ASP.NET Core, as tarefas em segundo plano podem ser implementadas como serviços hospedados. Um serviço hospedado é uma classe com lógica de tarefa em segundo plano que implementa a interface IHostedService. Este artigo fornece três exemplos de serviço hospedado:

Exibir ou baixar código de exemplo (como baixar)

Modelo de serviço de trabalho

O modelo de Serviço de Trabalho do ASP.NET Core fornece um ponto inicial para escrever aplicativos de serviço de execução prolongada. Um aplicativo criado com base no modelo do Serviço de Trabalho especifica o SDK de Trabalho em seu arquivo de projeto:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Para usar o modelo como base para um aplicativo de serviços hospedados:

  1. Criar um novo projeto.
  2. Selecione Serviço de Trabalho. Selecione Avançar.
  3. Forneça um nome ao projeto no campo Nome do projeto ou aceite o nome do projeto padrão. Selecione Criar.
  4. Na caixa de diálogo Criar um novo serviço de trabalho , selecione Criar.

Pacote

Um aplicativo baseado no modelo do Serviço de Trabalho usa o Microsoft.NET.Sdk.Worker SDK e tem uma referência de pacote explícita ao pacote Microsoft.Extensions.Hosting . Por exemplo, consulte o arquivo de projeto do aplicativo de exemplo (BackgroundTasksSample.csproj).

Para aplicativos Web que usam o Microsoft.NET.Sdk.Web SDK, o pacote Microsoft.Extensions.Hosting é referenciado implicitamente da estrutura compartilhada. Não é necessária uma referência explícita de pacote no arquivo de projeto do aplicativo.

Interface IHostedService

A IHostedService interface define dois métodos para objetos gerenciados pelo host:

StartAsync

StartAsync contém a lógica para iniciar a tarefa em segundo plano. StartAsync é chamado antes:

O comportamento padrão pode ser alterado para que o serviço hospedado seja StartAsync executado após a configuração do pipeline do aplicativo e ApplicationStarted seja chamado. Para alterar o comportamento padrão, adicione o serviço hospedado (VideosWatcher no exemplo a seguir) depois de chamar ConfigureWebHostDefaults:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
            .ConfigureServices(services =>
            {
                services.AddHostedService<VideosWatcher>();
            });
}

StopAsync

O token de cancelamento tem um tempo limite padrão de cinco segundos para indicar que o processo de desligamento não deve mais ser normal. Quando for solicitado um cancelamento no token:

  • Todas as demais operações em segundo plano que o aplicativo estiver executando deverão ser anuladas.
  • Todos os métodos chamados em StopAsync deverão retornar imediatamente.

No entanto, as tarefas não são abandonadas depois que o cancelamento é solicitado— o chamador aguarda que todas as tarefas sejam concluídas.

Se o aplicativo for desligado inesperadamente (por exemplo, em uma falha do processo do aplicativo), StopAsync não poderá ser chamado. Portanto, os métodos chamados ou operações realizadas em StopAsync talvez não ocorram.

Para estender o tempo limite de desligamento padrão de cinco segundos, defina:

O serviço hospedado é ativado uma única vez na inicialização do aplicativo e desligado normalmente durante o desligamento do aplicativo. Se um erro for gerado durante a execução da tarefa em segundo plano, Dispose deverá ser chamado mesmo se StopAsync não for chamado.

Classe base BackgroundService

BackgroundService é uma classe base para implementar uma execução longa IHostedService.

ExecuteAsync(CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna um Task que representa todo o tempo de vida do serviço em segundo plano. Nenhum serviço adicional é iniciado até que ExecuteAsync se torne assíncrono, como chamando await. Evite executar trabalhos longos e de inicialização de bloqueio.ExecuteAsync Os blocos de host em StopAsync(CancellationToken) aguardando ExecuteAsync a conclusão.

O token de cancelamento é disparado quando IHostedService.StopAsync é chamado. Sua implementação ExecuteAsync deve ser concluída imediatamente quando o token de cancelamento for acionado para desligar normalmente o serviço. Caso contrário, o serviço será desligado sem êxito no tempo limite de desligamento. Para obter mais informações, consulte a seção de interface IHostedService .

StartAsync deve ser limitado a tarefas de execução curta porque os serviços hospedados são executados sequencialmente e nenhum outro serviço é iniciado até StartAsync que seja executado até a conclusão. Tarefas de execução longa devem ser colocadas em ExecuteAsync. Para obter mais informações, consulte a origem de BackgroundService.

Tarefas em segundo plano temporizadas

Uma tarefa em segundo plano temporizada usa a classe System.Threading.Timer. O temporizador dispara o método DoWork da tarefa. O temporizador é desabilitado em StopAsync e descartado quando o contêiner de serviço é descartado em Dispose:

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

O Timer não aguarda a conclusão das DoWork execuções anteriores, portanto, a abordagem mostrada pode não ser adequada para todos os cenários. Interlocked.Increment é usado para incrementar o contador de execução como uma operação atômica, o que garante que vários threads não sejam atualizados executionCount simultaneamente.

O serviço está registrado em IHostBuilder.ConfigureServices (Program.cs) com o AddHostedService método de extensão:

services.AddHostedService<TimedHostedService>();

Consumindo um serviço com escopo em uma tarefa em segundo plano

Para usar serviços com escopo em um BackgroundService, crie um escopo. Por padrão, nenhum escopo é criado para um serviço hospedado.

O serviço da tarefa em segundo plano com escopo contém a lógica da tarefa em segundo plano. No exemplo a seguir:

  • O serviço é assíncrono. O método DoWork retorna uma Task. Para fins de demonstração, um atraso de dez segundos é aguardado no DoWork método.
  • Um ILogger é injetado no serviço.
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

O serviço hospedado cria um escopo para resolver o serviço de tarefa em segundo plano com escopo para chamar seu DoWork método. DoWork retorna um Task, que é aguardado em ExecuteAsync:

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Os serviços estão registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registrado com o método de AddHostedService extensão:

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

Tarefas em segundo plano na fila

Uma fila de tarefas em segundo plano é baseada no .NET 4.x QueueBackgroundWorkItem:

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

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

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

No exemplo a seguir QueueHostedService :

  • O BackgroundProcessing método retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são desqueadas e executadas em BackgroundProcessing.
  • Os itens de trabalho são aguardados antes que o serviço pare.StopAsync
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

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

  • O IBackgroundTaskQueue é injetado no MonitorLoop serviço.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem é 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 (Task.Delay).
    • Uma try-catch instrução será interceptada OperationCanceledException se a tarefa for cancelada.
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue, 
        ILogger<MonitorLoop> logger, 
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("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(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {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 Background Task {Guid} is running. " + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

Os serviços estão registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registrado com o método de AddHostedService extensão:

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx => {
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoop é iniciado em Program.Main:

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

Recursos adicionais