Фоновые задачи с размещенными службами в ASP.NET Core

Автор: Джау Ли Хуань (Jeow Li Huan)

Примечание.

Это не последняя версия этой статьи. Сведения о текущем выпуске см. в ASP.NET версии Core 8.0 этой статьи.

В ASP.NET Core фоновые задачи реализуются как размещенные службы. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService. Эта статья содержит три примера размещенных служб:

Шаблон службы рабочей роли

Шаблон службы рабочей роли ASP.NET Core может служить отправной точкой для написания длительно выполняющихся приложений служб. Приложение, созданное из шаблона рабочей службы, указывает рабочий пакет SDK в файле проекта:

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

Чтобы использовать шаблон в качестве основы для приложения размещенных служб, выполните указанные ниже действия.

  1. Создание проекта
  2. Выберите службу рабочей роли. Выберите Далее.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Выберите Далее.
  4. В диалоговом окне Дополнительные сведения выберите Платформа. Нажмите кнопку создания.

Пакет

Приложение, основанное на шаблоне рабочей службы, использует пакет SDK для Microsoft.NET.Sdk.Worker и имеет явную ссылку на пакет Microsoft.Extensions.Hosting. Например, см. файл проекта примера приложения (BackgroundTasksSample.csproj).

Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на пакет Microsoft. Extensions. Hosting указывается неявным образом из общей платформы. Явная ссылка на пакет в файле проекта приложения не требуется.

Интерфейс IHostedService

Интерфейс IHostedService определяет два метода для объектов, которые управляются узлом:

StartAsync

StartAsync(CancellationToken) содержит логику для запуска фоновой задачи. Первым вызывается StartAsync:

  • Настраивается конвейер обработки запросов приложения.
  • Запускается сервер и активируется IApplicationLifetime.ApplicationStarted.

StartAsync следует ограничить короткими задачами, так как размещенные службы выполняются последовательно, и никакие другие службы не запускаются до завершения StartAsync.

StopAsync

Маркер отмены по умолчанию имеет 30 секундное время ожидания, чтобы указать, что процесс завершения работы больше не должен быть изящным. При запросе отмены происходит следующее:

  • должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
  • должны быть незамедлительно возвращены все методы, вызываемые в StopAsync.

Однако после запроса отмены выполнение задач не прекращается — вызывающий объект ожидает завершения всех задач.

Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync может быть невозможным.

Чтобы продлить время ожидания завершения работы по умолчанию 30 секунд, задайте следующую настройку:

Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать Dispose, даже если StopAsync не вызывается.

Базовый класс BackgroundService

BackgroundService — это базовый класс для реализации долго выполняющегося интерфейса IHostedService.

ExecuteAsync(CancellationToken) вызывается для запуска фоновой службы. Реализация возвращает значение Task, представляющее все время существования фоновой службы. Дальнейшие службы не запустятся до тех пор, пока ExecuteAsync не станет асинхронной, например, путем вызова await. Старайтесь не выполнять функцию в течение длительного времени, так как инициализация в ExecuteAsync будет заблокирована. Блоки узлов в StopAsync(CancellationToken) ожидают завершения ExecuteAsync.

Токен отмены активируется при вызове IHostedService.StopAsync. При выдаче токена отмены реализация ExecuteAsync должна быстро завершиться для корректного завершения работы службы. В противном случае служба некорректно завершает работу при истечении времени ожидания завершения работы. Дополнительные сведения см. в разделе об интерфейсе IHostedService.

Дополнительные сведения см. в описании исходного кода BackgroundService.

Фоновые задачи с заданным временем

Для фоновых задач с заданным временем используется класс System.Threading.Timer. Таймер запускает метод DoWork задачи. Таймер отключается методом StopAsync и удаляется при удалении контейнера службы методом 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();
    }
}

Timer не ждет завершения предыдущего метода DoWork, поэтому приведенный подход может подойти не для всех сценариев. Interlocked.Increment используется для увеличения значений счетчика выполнения в виде атомарной операции. Благодаря этому несколько потоков не будут обновлять executionCount одновременно.

Служба зарегистрирована в IHostBuilder.ConfigureServices (Program.cs) с методом расширения AddHostedService:

services.AddHostedService<TimedHostedService>();

Использование службы с заданной областью в фоновой задаче

Чтобы использовать службы с заданной областью в BackgroundService, создайте область. Для размещенной службы по умолчанию не создается область.

Служба фоновой задачи с заданной областью содержит логику фоновой задачи. В следующем примере :

  • Служба является асинхронной. Метод DoWork возвращает Task. В демонстрационных целях в методе DoWork ожидается задержка в десять секунд.
  • В службу вставляется ILogger.
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);
        }
    }
}

Размещенная служба создает область для разрешения службы фоновой задачи с заданной областью, чтобы вызвать ее метод DoWork: DoWork возвращает объект Task, ожидаемый в 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);
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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

Фоновые задачи в очереди

Очередь фоновых задач основана на QueueBackgroundWorkItem .NET 4.x.

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;
    }
}

В следующем примере QueueHostedService:

  • Метод BackgroundProcessing возвращает объект Task, ожидаемый в ExecuteAsync:
  • Фоновые задачи в очереди выводятся из очереди и выполняются в BackgroundProcessing:
  • Рабочие элементы ожидают остановки службы через 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);
    }
}

Служба MonitorLoop обрабатывает задачи постановки в очередь для размещенной службы при выборе на устройстве ввода ключа w:

  • В службу MonitorLoop внедряется IBackgroundTaskQueue.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem вызывается для постановки рабочего элемента в очередь:
  • Рабочий элемент имитирует долго выполняющуюся фоновую задачу:
    • Выполняется три 5-секундных задержки (Task.Delay).
    • Оператор try-catch перехватывается OperationCanceledException, если задача отменена.
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);
        }
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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 запущен в Program.cs:

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

Асинхронная фоновая задача

Следующий код создает асинхронную фоновую задачу с асинхронным временем:

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

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

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

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

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

Собственный AOT

Шаблоны службы рабочих ролей поддерживают собственный код .NET перед временем (AOT) с флагом --aot :

  1. Создание проекта
  2. Выберите службу рабочей роли. Выберите Далее.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Выберите Далее.
  4. В диалоговом окне Дополнительные сведения выполните следующие действия.
  5. Выберите платформу.
  6. Установите флажок "Включить собственную публикацию AOT" проверка box.
  7. Нажмите кнопку создания.

Параметр AOT добавляется <PublishAot>true</PublishAot> в файл проекта:


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

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
+   <PublishAot>true</PublishAot>
    <UserSecretsId>dotnet-WorkerWithAot-e94b2</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.4.23259.5" />
  </ItemGroup>
</Project>

Дополнительные ресурсы

В ASP.NET Core фоновые задачи реализуются как размещенные службы. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService. Эта статья содержит три примера размещенных служб:

Шаблон службы рабочей роли

Шаблон службы рабочей роли ASP.NET Core может служить отправной точкой для написания длительно выполняющихся приложений служб. Приложение, созданное из шаблона рабочей службы, указывает рабочий пакет SDK в файле проекта:

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

Чтобы использовать шаблон в качестве основы для приложения размещенных служб, выполните указанные ниже действия.

  1. Создание проекта
  2. Выберите службу рабочей роли. Выберите Далее.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Выберите Далее.
  4. В диалоговом окне Дополнительные сведения выберите Платформа. Нажмите кнопку создания.

Пакет

Приложение, основанное на шаблоне рабочей службы, использует пакет SDK для Microsoft.NET.Sdk.Worker и имеет явную ссылку на пакет Microsoft.Extensions.Hosting. Например, см. файл проекта примера приложения (BackgroundTasksSample.csproj).

Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на пакет Microsoft. Extensions. Hosting указывается неявным образом из общей платформы. Явная ссылка на пакет в файле проекта приложения не требуется.

Интерфейс IHostedService

Интерфейс IHostedService определяет два метода для объектов, которые управляются узлом:

StartAsync

StartAsync(CancellationToken) содержит логику для запуска фоновой задачи. Первым вызывается StartAsync:

  • Настраивается конвейер обработки запросов приложения.
  • Запускается сервер и активируется IApplicationLifetime.ApplicationStarted.

StartAsync следует ограничить короткими задачами, так как размещенные службы выполняются последовательно, и никакие другие службы не запускаются до завершения StartAsync.

StopAsync

Маркер отмены по умолчанию имеет 30 секундное время ожидания, чтобы указать, что процесс завершения работы больше не должен быть изящным. При запросе отмены происходит следующее:

  • должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
  • должны быть незамедлительно возвращены все методы, вызываемые в StopAsync.

Однако после запроса отмены выполнение задач не прекращается — вызывающий объект ожидает завершения всех задач.

Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync может быть невозможным.

Чтобы продлить время ожидания завершения работы по умолчанию 30 секунд, задайте следующую настройку:

Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать Dispose, даже если StopAsync не вызывается.

Базовый класс BackgroundService

BackgroundService — это базовый класс для реализации долго выполняющегося интерфейса IHostedService.

ExecuteAsync(CancellationToken) вызывается для запуска фоновой службы. Реализация возвращает значение Task, представляющее все время существования фоновой службы. Дальнейшие службы не запустятся до тех пор, пока ExecuteAsync не станет асинхронной, например, путем вызова await. Старайтесь не выполнять функцию в течение длительного времени, так как инициализация в ExecuteAsync будет заблокирована. Блоки узлов в StopAsync(CancellationToken) ожидают завершения ExecuteAsync.

Токен отмены активируется при вызове IHostedService.StopAsync. При выдаче токена отмены реализация ExecuteAsync должна быстро завершиться для корректного завершения работы службы. В противном случае служба некорректно завершает работу при истечении времени ожидания завершения работы. Дополнительные сведения см. в разделе об интерфейсе IHostedService.

Дополнительные сведения см. в описании исходного кода BackgroundService.

Фоновые задачи с заданным временем

Для фоновых задач с заданным временем используется класс System.Threading.Timer. Таймер запускает метод DoWork задачи. Таймер отключается методом StopAsync и удаляется при удалении контейнера службы методом 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();
    }
}

Timer не ждет завершения предыдущего метода DoWork, поэтому приведенный подход может подойти не для всех сценариев. Interlocked.Increment используется для увеличения значений счетчика выполнения в виде атомарной операции. Благодаря этому несколько потоков не будут обновлять executionCount одновременно.

Служба зарегистрирована в IHostBuilder.ConfigureServices (Program.cs) с методом расширения AddHostedService:

services.AddHostedService<TimedHostedService>();

Использование службы с заданной областью в фоновой задаче

Чтобы использовать службы с заданной областью в BackgroundService, создайте область. Для размещенной службы по умолчанию не создается область.

Служба фоновой задачи с заданной областью содержит логику фоновой задачи. В следующем примере :

  • Служба является асинхронной. Метод DoWork возвращает Task. В демонстрационных целях в методе DoWork ожидается задержка в десять секунд.
  • В службу вставляется ILogger.
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);
        }
    }
}

Размещенная служба создает область для разрешения службы фоновой задачи с заданной областью, чтобы вызвать ее метод DoWork: DoWork возвращает объект Task, ожидаемый в 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);
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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

Фоновые задачи в очереди

Очередь фоновых задач основана на QueueBackgroundWorkItem .NET 4.x.

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;
    }
}

В следующем примере QueueHostedService:

  • Метод BackgroundProcessing возвращает объект Task, ожидаемый в ExecuteAsync:
  • Фоновые задачи в очереди выводятся из очереди и выполняются в BackgroundProcessing:
  • Рабочие элементы ожидают остановки службы через 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);
    }
}

Служба MonitorLoop обрабатывает задачи постановки в очередь для размещенной службы при выборе на устройстве ввода ключа w:

  • В службу MonitorLoop внедряется IBackgroundTaskQueue.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem вызывается для постановки рабочего элемента в очередь:
  • Рабочий элемент имитирует долго выполняющуюся фоновую задачу:
    • Выполняется три 5-секундных задержки (Task.Delay).
    • Оператор try-catch перехватывается OperationCanceledException, если задача отменена.
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);
        }
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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 запущен в Program.cs:

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

Асинхронная фоновая задача

Следующий код создает асинхронную фоновую задачу с асинхронным временем:

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

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

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

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

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

Дополнительные ресурсы

В ASP.NET Core фоновые задачи реализуются как размещенные службы. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService. Эта статья содержит три примера размещенных служб:

Просмотреть или скачать образец кода (описание загрузки)

Шаблон службы рабочей роли

Шаблон службы рабочей роли ASP.NET Core может служить отправной точкой для написания длительно выполняющихся приложений служб. Приложение, созданное из шаблона рабочей службы, указывает рабочий пакет SDK в файле проекта:

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

Чтобы использовать шаблон в качестве основы для приложения размещенных служб, выполните указанные ниже действия.

  1. Создание проекта
  2. Выберите службу рабочей роли. Выберите Далее.
  3. В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Нажмите кнопку создания.
  4. В диалоговом окне Создать службу рабочей роли выберите Создать.

Пакет

Приложение, основанное на шаблоне рабочей службы, использует пакет SDK для Microsoft.NET.Sdk.Worker и имеет явную ссылку на пакет Microsoft.Extensions.Hosting. Например, см. файл проекта примера приложения (BackgroundTasksSample.csproj).

Для веб-приложений, использующих пакет SDK Microsoft.NET.Sdk.Web, ссылка на пакет Microsoft. Extensions. Hosting указывается неявным образом из общей платформы. Явная ссылка на пакет в файле проекта приложения не требуется.

Интерфейс IHostedService

Интерфейс IHostedService определяет два метода для объектов, которые управляются узлом:

StartAsync

StartAsync содержит логику для запуска фоновой задачи. Первым вызывается StartAsync:

  • Настраивается конвейер обработки запросов приложения.
  • Запускается сервер и активируется IApplicationLifetime.ApplicationStarted.

Поведение по умолчанию можно изменить таким образом, чтобы StartAsync размещенной службы выполнялся после настройки конвейера приложения и вызова ApplicationStarted. Чтобы изменить поведение по умолчанию, добавьте размещенную службу (VideosWatcher в следующем примере) после вызова 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

Токен отмены использует заданное по умолчанию 5-секундное время ожидания, указывающее, что процесс завершения работы больше не должен быть нормальным. При запросе отмены происходит следующее:

  • должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
  • должны быть незамедлительно возвращены все методы, вызываемые в StopAsync.

Однако после запроса отмены выполнение задач не прекращается — вызывающий объект ожидает завершения всех задач.

Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync может быть невозможным.

Чтобы увеличить время ожидания завершения работы по умолчанию (пять секунд), установите следующие значения:

Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать Dispose, даже если StopAsync не вызывается.

Базовый класс BackgroundService

BackgroundService — это базовый класс для реализации долго выполняющегося интерфейса IHostedService.

ExecuteAsync(CancellationToken) вызывается для запуска фоновой службы. Реализация возвращает значение Task, представляющее все время существования фоновой службы. Дальнейшие службы не запустятся до тех пор, пока ExecuteAsync не станет асинхронной, например, путем вызова await. Старайтесь не выполнять функцию в течение длительного времени, так как инициализация в ExecuteAsync будет заблокирована. Блоки узлов в StopAsync(CancellationToken) ожидают завершения ExecuteAsync.

Токен отмены активируется при вызове IHostedService.StopAsync. При выдаче токена отмены реализация ExecuteAsync должна быстро завершиться для корректного завершения работы службы. В противном случае служба некорректно завершает работу при истечении времени ожидания завершения работы. Дополнительные сведения см. в разделе об интерфейсе IHostedService.

StartAsync следует ограничить короткими задачами, так как размещенные службы выполняются последовательно, и никакие другие службы не запускаются до завершения StartAsync. Длительные задачи должны размещаться в ExecuteAsync. Дополнительные сведения см. в описании BackgroundService.

Фоновые задачи с заданным временем

Для фоновых задач с заданным временем используется класс System.Threading.Timer. Таймер запускает метод DoWork задачи. Таймер отключается методом StopAsync и удаляется при удалении контейнера службы методом 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();
    }
}

Timer не ждет завершения предыдущего метода DoWork, поэтому приведенный подход может подойти не для всех сценариев. Interlocked.Increment используется для увеличения значений счетчика выполнения в виде атомарной операции. Благодаря этому несколько потоков не будут обновлять executionCount одновременно.

Служба зарегистрирована в IHostBuilder.ConfigureServices (Program.cs) с методом расширения AddHostedService:

services.AddHostedService<TimedHostedService>();

Использование службы с заданной областью в фоновой задаче

Чтобы использовать службы с заданной областью в BackgroundService, создайте область. Для размещенной службы по умолчанию не создается область.

Служба фоновой задачи с заданной областью содержит логику фоновой задачи. В следующем примере :

  • Служба является асинхронной. Метод DoWork возвращает Task. В демонстрационных целях в методе DoWork ожидается задержка в десять секунд.
  • В службу вставляется ILogger.
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);
        }
    }
}

Размещенная служба создает область для разрешения службы фоновой задачи с заданной областью, чтобы вызвать ее метод DoWork: DoWork возвращает объект Task, ожидаемый в 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);
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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

Фоновые задачи в очереди

Очередь фоновых задач основана на QueueBackgroundWorkItem .NET 4.x.

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;
    }
}

В следующем примере QueueHostedService:

  • Метод BackgroundProcessing возвращает объект Task, ожидаемый в ExecuteAsync:
  • Фоновые задачи в очереди выводятся из очереди и выполняются в BackgroundProcessing:
  • Рабочие элементы ожидают остановки службы через 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);
    }
}

Служба MonitorLoop обрабатывает задачи постановки в очередь для размещенной службы при выборе на устройстве ввода ключа w:

  • В службу MonitorLoop внедряется IBackgroundTaskQueue.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem вызывается для постановки рабочего элемента в очередь:
  • Рабочий элемент имитирует долго выполняющуюся фоновую задачу:
    • Выполняется три 5-секундных задержки (Task.Delay).
    • Оператор try-catch перехватывается OperationCanceledException, если задача отменена.
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);
        }
    }
}

Службы регистрируются в IHostBuilder.ConfigureServices (Program.cs). Размещенная служба регистрируется с использованием метода расширения AddHostedService:

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 запущен в Program.Main:

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

Дополнительные ресурсы