В настоящее время я пытаюсь создать консольное приложение в .NET 7, которое может одновременно запускать несколько экземпляров службы.
Program.cs
файл выглядит так:
private static async Task Main(string[] args)
{
await Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<ConsoleHostedService>();
services.AddScraperServices();
})
.RunConsoleAsync();
}
где AddScraperServices()
выглядит так:
public static IServiceCollection AddScraperServices(this IServiceCollection services)
{
services.AddTransient<IScrapingService, ScrapingService>();
services.AddScoped(provider => new SemaphoreSlim(2));
return services;
}
и ConsoleHostedService вот так:
internal sealed class ConsoleHostedService : IHostedService
{
// .. properties and constructor here
public Task StartAsync(CancellationToken cancellationToken)
{
_appLifetime.ApplicationStarted.Register(() =>
{
Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
await _semaphore.WaitAsync(cancellationToken);
try
{
using (var scope = _serviceProvider.CreateScope())
{
var scrapingService = scope.ServiceProvider.GetRequiredService<IScrapingService>();
await scrapingService.ScrapeAsync();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception!");
}
finally
{
_semaphore.Release();
}
}
});
});
return Task.CompletedTask;
}
}
ScrapingSerivce, который используется в ConsoleHostedService, выглядит так:
public class ScrapingService : IScrapingService
{
// .. properties and constructor here
public async Task ScrapeAsync()
{
await _semaphore.WaitAsync();
try
{
// do stuff
}
finally
{
_semaphore.Release();
}
}
}
Насколько я понимаю, Semaphore теперь должен создавать два экземпляра ScrapingService, работающих параллельно друг другу. Спойлер: это не так.
Возможно, важно упомянуть, что Semaphore должен создавать новый экземпляр, если он уже завершен. Это означает, что я всегда хочу, чтобы количество экземпляров равнялось максимальному количеству экземпляров.
Кто-нибудь знает, где моя проблема? Я очень ценю любую помощь, ура!
Semaphore
теперь должно создавать два экземпляраScrapingService
, работающих параллельно друг другу.
Наверное просто придирка и я педант - SemaphoreSlim
ничего не запускает это просто примитив используемый для синхронизации.
Насколько я вижу, в ConsoleHostedService.StartAsync
вы запускаете только одну задачу, которая запускает бесконечный цикл, ожидающий каждого вызова ScrapeAsync
- await scrapingService.ScrapeAsync();
, поэтому следующая итерация не начнется, пока текущая не будет завершена, поэтому здесь нет параллельных вызовов.
Лично я бы даже не заморачивался с семафором, а просто использовал некоторые настройки для определения параллелизма и передал его размещенной службе (также я бы использовал BackgroundService
вместо IHostedService
, см. документы). Что-то в этом роде:
class MyBackgroundService : BackgroundService
{
private readonly MyBackgroundServiceSettings _settings;
public MyBackgroundService(MyBackgroundServiceSettings settings)
{
_settings = settings;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var tasks = Enumerable.Range(0, _settings.ParallelCount)
.Select(_ => Task.Run(async () =>
{
while (stoppingToken.IsCancellationRequested)
{
// ...
}
}))
.ToArray();
await Task.WhenAll(tasks);
}
}
Если по какой-то причине существует вероятность того, что ScrapingService
разрешено где-то еще в приложении (хотя я бы посоветовал избегать этого), и вы все еще хотите ограничить его одновременные вызовы, то я настоятельно рекомендую абстрагироваться от SemaphoreSlim
(что-то вроде IScrapingServiceLimiter
, возможно, используйте ), чтобы избежать регистрации services.AddScoped(provider => new SemaphoreSlim(2));
(что выглядит немного странно для меня, TBH).
P.S.
Также я бы рекомендовал передать CancellationToken stoppingToken
и использовать его в IScrapingService.ScrapeAsync
с соответствующими изменениями в коде.
В случае сбоя кода, представленного // ...
, одна задача будет завершена как ошибочная, но другие задачи продолжат выполняться. Служба будет продолжать работать с уменьшенной степенью параллелизма. Желательно ли такое поведение?
@MaikHasler Теодор делает хорошее замечание - убедитесь, что обработчик отказоустойчив.
@TheodorZoulias В моем случае было бы абсолютно нормально иметь меньшую степень параллелизма. В любом случае, может быть кому-то нужен другой, более отказоустойчивый вариант. Может быть, вы хотите добавить свой, если вы его имеете в виду :)
@MaikHasler как раз наоборот. Я бы посоветовал полностью отказоустойчивую реализацию, в которой один неисправный рабочий процесс вызвал бы немедленную отмену всех других рабочих процессов с последующим прекращением процесса. Я не думаю, что процесс с несколькими, или половиной, или всеми мертвыми работниками, кроме одного, — это здоровая ситуация.
Мне нравится идея с фоновым сервисом - теперь все работает нормально! Большое спасибо.