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.