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 injeção de dependência (di).
  • 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 de serviço de trabalho especifica o SDK do trabalhador 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 de serviço de trabalho usa o Microsoft.NET.Sdk.Worker SDK e tem uma referência de pacote explícita 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. Uma referência de pacote explícita no arquivo de projeto do aplicativo não é necessária.

Interface IHostedService

A IHostedService interface define dois métodos para objetos que são gerenciados pelo host:

StartAsync

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

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é que StartAsync o seja executado até 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 todas as tarefas serem 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 longa execução IHostedService .

ExecuteAsync (CancellationToken) é chamado para executar o serviço em segundo plano. A implementação retorna um Task que representa o tempo de vida inteiro do serviço em segundo plano. Nenhum serviço adicional é iniciado até que ExecuteAsync se torne assíncrono, por exemplo, chamando await . Evite executar tempo demorado, bloqueando o trabalho de inicialização no ExecuteAsync . Os blocos de host em StopAsync (CancellationToken) aguardando ExecuteAsync para serem concluídos.

O token de cancelamento é disparado quando IHostedService. StopAsync é chamado. Sua implementação do ExecuteAsync deve ser concluída imediatamente quando o token de cancelamento é acionado para desligar o serviço normalmente. Caso contrário, o serviço será desligado sem nenhum normal no tempo limite do desligamento. Para obter mais informações, consulte a seção 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 as execuções anteriores do DoWork serem concluídas, portanto, a abordagem mostrada pode não ser adequada para cada cenário. 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 método de AddHostedService 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 é esperado 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 são registrados no IHostBuilder.ConfigureServices ( Program.cs ). O serviço hospedado está 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 QueueHostedService seguir:

  • O BackgroundProcessing método retorna um Task, que é aguardado em ExecuteAsync.
  • As tarefas em segundo plano na fila são enfileirasdas e executadas no BackgroundProcessing.
  • Os itens de trabalho são aguardados antes que o serviço pare no 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 enquete para o serviço hospedado sempre que w a chave é selecionada em um dispositivo de entrada:

  • O IBackgroundTaskQueue é injetado no MonitorLoop serviço.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem é chamado para enquete 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 intercepta OperationCanceledException se a tarefa é 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 são registrados em IHostBuilder.ConfigureServices (Program.cs). O serviço hospedado é registrado com o método AddHostedService de 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:

  • 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 a DI (injeção de dependência).
  • Tarefas em segundo plano na fila que são executadas sequencialmente.

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 o modelo de Serviço de Trabalho especifica o SDK do 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 Microsoft.NET.Sdk.Worker Trabalho usa o 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. Uma referência de pacote explícita no arquivo de projeto do aplicativo não é necessária.

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 de:

O comportamento padrão pode ser alterado para que o serviço StartAsync hospedado seja executado após o pipeline do aplicativo ter sido configurado e ApplicationStarted ser 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 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 um de 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 outro serviço é iniciado até que ExecuteAsync se torne assíncrono, como chamando await. Evite executar o trabalho de inicialização longo e bloqueando no ExecuteAsync. Os blocos de host em StopAsync(CancellationToken) aguardando a ExecuteAsync conclusão.

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

StartAsync deve ser limitado a tarefas de execução curta porque os serviços hospedados são executados sequencialmente e nenhum outro StartAsync serviço é iniciado até 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 para 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 as execuções anteriores do DoWork serem concluídas, portanto, a abordagem mostrada pode não ser adequada para cada cenário. 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 método de AddHostedService 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 é esperado 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 são registrados no IHostBuilder.ConfigureServices ( Program.cs ). O serviço hospedado está 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 é esperado em ExecuteAsync .
  • As tarefas em segundo plano na fila são removidas da fila e executadas no BackgroundProcessing .
  • Os itens de trabalho são aguardados antes de o serviço parar 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:

  • 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:
    • São executados atrasos de 3 5 segundos ( Task.Delay ).
    • Uma try-catch instrução intercepta OperationCanceledException se a tarefa é 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 são registrados no IHostBuilder.ConfigureServices ( Program.cs ). O serviço hospedado está 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