Фоновые задачи с размещенными службами в ASP.NET Core
Автор: Джау Ли Хуань (Jeow Li Huan)
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 9 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 9 этой статьи.
В ASP.NET Core фоновые задачи реализуются как размещенные службы. Размещенная служба — это класс с логикой фоновой задачи, реализующий интерфейс IHostedService. Эта статья содержит три примера размещенных служб:
- Фоновая задача, которая выполняется по таймеру.
- Размещенная служба, которая активирует службу с заданной областью. Служба с заданной областью может использовать внедрение зависимостей.
- Очередь фоновых задач, которые выполняются последовательно.
Шаблон службы рабочей роли
Шаблон службы рабочей роли ASP.NET Core может служить отправной точкой для написания длительно выполняющихся приложений служб. Приложение, созданное из шаблона рабочей службы, указывает рабочий пакет SDK в файле проекта:
<Project Sdk="Microsoft.NET.Sdk.Worker">
Чтобы использовать шаблон в качестве основы для приложения размещенных служб, выполните указанные ниже действия.
- Создание проекта
- Выберите службу рабочей роли. Выберите Далее.
- В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Выберите Далее.
- В диалоговом окне Дополнительные сведения выберите Платформа. Нажмите кнопку создания.
Пакет
Приложение, основанное на шаблоне рабочей службы, использует пакет 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
- StopAsync(CancellationToken) запускается, когда происходит нормальное завершение работы узла.
StopAsync
содержит логику для завершения фоновой задачи. Реализуйте IDisposable и методы завершения (деструкторы) для освобождения неуправляемых ресурсов.
Маркер отмены по умолчанию имеет 30 секундное время ожидания, чтобы указать, что процесс завершения работы больше не должен быть изящным. При запросе отмены происходит следующее:
- должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
- должны быть незамедлительно возвращены все методы, вызываемые в
StopAsync
.
Однако после запроса отмены выполнение задач не прекращается — вызывающий объект ожидает завершения всех задач.
Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync
может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync
может быть невозможным.
Чтобы продлить время ожидания завершения работы по умолчанию 30 секунд, задайте следующую настройку:
- ShutdownTimeout при использовании универсального узла. Дополнительные сведения см. в статье Универсальный узел .NET в ASP.NET Core.
- Параметр конфигурации узла для времени ожидания завершения работы при использовании веб-узла. Подробные сведения см. в статье Веб-узел ASP.NET Core.
Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать 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, если задача отменена.
- Выполняется три 5-секундных задержки (
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
:
- Создание проекта
- Выберите службу рабочей роли. Выберите Далее.
- В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Выберите Далее.
- В диалоговом окне Дополнительные сведения выполните следующие действия.
- Выберите платформу.
- Установите флажок "Включить собственную публикацию AOT".
- Нажмите кнопку создания.
Параметр 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">
Чтобы использовать шаблон в качестве основы для приложения размещенных служб, выполните указанные ниже действия.
- Создание проекта
- Выберите службу рабочей роли. Выберите Далее.
- В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Выберите Далее.
- В диалоговом окне Дополнительные сведения выберите Платформа. Нажмите кнопку создания.
Пакет
Приложение, основанное на шаблоне рабочей службы, использует пакет 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
- StopAsync(CancellationToken) запускается, когда происходит нормальное завершение работы узла.
StopAsync
содержит логику для завершения фоновой задачи. Реализуйте IDisposable и методы завершения (деструкторы) для освобождения неуправляемых ресурсов.
Маркер отмены по умолчанию имеет 30 секундное время ожидания, чтобы указать, что процесс завершения работы больше не должен быть изящным. При запросе отмены происходит следующее:
- должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
- должны быть незамедлительно возвращены все методы, вызываемые в
StopAsync
.
Однако после запроса отмены выполнение задач не прекращается — вызывающий объект ожидает завершения всех задач.
Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync
может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync
может быть невозможным.
Чтобы продлить время ожидания завершения работы по умолчанию 30 секунд, задайте следующую настройку:
- ShutdownTimeout при использовании универсального узла. Дополнительные сведения см. в статье Универсальный узел .NET в ASP.NET Core.
- Параметр конфигурации узла для времени ожидания завершения работы при использовании веб-узла. Подробные сведения см. в статье Веб-узел ASP.NET Core.
Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать 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, если задача отменена.
- Выполняется три 5-секундных задержки (
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">
Чтобы использовать шаблон в качестве основы для приложения размещенных служб, выполните указанные ниже действия.
- Создание проекта
- Выберите службу рабочей роли. Выберите Далее.
- В поле Имя проекта укажите имя проекта или оставьте имя по умолчанию. Нажмите кнопку создания.
- В диалоговом окне Создать службу рабочей роли выберите Создать.
Пакет
Приложение, основанное на шаблоне рабочей службы, использует пакет 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
- StopAsync(CancellationToken) запускается, когда происходит нормальное завершение работы узла.
StopAsync
содержит логику для завершения фоновой задачи. Реализуйте IDisposable и методы завершения (деструкторы) для освобождения неуправляемых ресурсов.
Токен отмены использует заданное по умолчанию 5-секундное время ожидания, указывающее, что процесс завершения работы больше не должен быть нормальным. При запросе отмены происходит следующее:
- должны быть прерваны все оставшиеся фоновые операции, выполняемые приложением;
- должны быть незамедлительно возвращены все методы, вызываемые в
StopAsync
.
Однако после запроса отмены выполнение задач не прекращается — вызывающий объект ожидает завершения всех задач.
Если приложение завершает работу неожиданно (например, при сбое процесса приложения), StopAsync
может не вызываться. Поэтому вызов методов или выполнение операций в StopAsync
может быть невозможным.
Чтобы увеличить время ожидания завершения работы по умолчанию (пять секунд), установите следующие значения:
- ShutdownTimeout при использовании универсального узла. Дополнительные сведения см. в статье Универсальный узел .NET в ASP.NET Core.
- Параметр конфигурации узла для времени ожидания завершения работы при использовании веб-узла. Подробные сведения см. в статье Веб-узел ASP.NET Core.
Размещенная служба активируется при запуске приложения и нормально завершает работу при завершении работы приложения. Если во время выполнения задачи в фоновом режиме возникает ошибка, необходимо вызвать 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, если задача отменена.
- Выполняется три 5-секундных задержки (
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();
Дополнительные ресурсы
ASP.NET Core