В .NET 8 были выпущены интересные изменения в библиотеке
Microsoft.Extensions.Hosting, на которые многие почему-то не обратили внимания.
Данная либа предоставляет возможности для создания фоновых задач в шаблоне
ASP.NET приложения, а также для создания отдельных Worker-сервисов без привязки к веб-серверу (что, например, хорошо подходит для создания микросервисов, работающих с очередью брокера сообщений).
Приложение может содержать одну или несколько таких задач, реализующих
IHostedService и регистрирующихся в контейнере внедрения зависимостей. Microsoft предоставляет готовую реализацию интерфейса
IHostedService в виде базового абстрактного класса
BackgroundService, предоставляет общую менимальную логику методов
StartAsync() и
StopAsync(), которая достаточно большинству фоновых задач. Чтобы использовать
BackgroundService, необходимо унаследоваться от данного класса и реализуют абстрактный метод
ExecuteAsync() с логикой фоновой задачи.
public class WorkerServiceOne : BackgroundService
{
private readonly ILogger<WorkerServiceOne> _logger;
public WorkerServiceOne(ILogger<WorkerServiceOne> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker 1 running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
Для запуска служб создаем объект хоста и запускаем его. Вместе с запуском хоста будут запущены и все зарегистрированные службы.
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<WorkerServiceOne>();
services.AddHostedService<WorkerServiceTwo>();
})
.Build();
host.Run();
Таким образом, каждая задача
IHostedService, зарегистрированная в di-контейнере, запускается последовательно путем вызова метода
StartAsync(). То есть для запуска следующий задачи выполняется ожидание завершения метод
StartAsync() предыдущей задачи. Очевидно, что медленный запуск одной из служб тормозит развертывание всего приложения. А при остановке приложения выполняется такая же синхронная логика с
StopAsync(). Однако для
StopAsync() настраивается таймаут завершения и нужно учитывать время завершения каждого конкретного сервиса, а если завершение одной из служб будет занимать слишком много времени, весь процесс завершения приложения может завершаться аварийно.
Для решения этих проблем в .NET 8 была добавлена возможность "параллельного" запуска и завершения, регистрируемых служб. Для этого теперь существуют опции конфигурации
ServicesStartConcurrently и
ServicesStopConcurrently.
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.Configure<HostOptions>(options =>
{
options.ServicesStartConcurrently = true;
options.ServicesStopConcurrently = true;
});
services.AddHostedService<WorkerServiceOne>();
services.AddHostedService<WorkerServiceTwo>();
})
.Build();
host.Run();
Сейчас код реализации "параллельного" запуска служб хоста выглядит примерно следующим образом:
if (_options.ServicesStartConcurrently)
{
List<Task> tasks = new List<Task>();
foreach (IHostedService hostedService in _hostedServices)
{
tasks.Add(Task.Run(() => StartAndTryToExecuteAsync(hostedService, combinedCancellationToken), combinedCancellationToken));
}
Task groupedTasks = Task.WhenAll(tasks);
}
То есть задачи по-прежнему запускаются в том же порядке, в котором они регистрируются в di, но они не ожидаются на этом этапе. Задачи добавляются в список, и только после запуска каждой задачи
WhenAll используется для ожидания завершения
StartAsync для каждой службы. В этом режиме запуск займет столько же времени, сколько самый медленный вызов
StartAsync, что позволяет приложению в целом запуститься быстрее. Аналогичная история и с
StopAsync.