如何在C#/.NET 7中并发运行多个服务实例?
问题分析与修复方案
核心问题点
SemaphoreSlim 生命周期错误
你将SemaphoreSlim注册为Scoped,但全局并发控制需要单例实例。Scoped生命周期会导致每个服务Scope生成独立的信号量,完全无法起到全局限流的作用。双重信号量等待
ConsoleHostedService中已经通过_semaphore.WaitAsync()获取了许可,ScrapingService的ScrapeAsync方法中又重复执行了一次等待操作。这相当于每次执行任务需要消耗2个许可,而你的信号量初始值仅为2,最终导致同一时间只能有1个任务运行,完全达不到并行效果。工作任务启动逻辑低效
当前的循环逻辑是串行等待信号量后启动单个任务,无法持续保持最大并发数。当一个任务完成后,才会触发下一次循环,中间存在空窗期。
修复代码
1. 修正SemaphoreSlim的注册为Singleton
public static IServiceCollection AddScraperServices(this IServiceCollection services) { services.AddTransient<IScrapingService, ScrapingService>(); // 改为Singleton,全局共享一个信号量实例 services.AddSingleton(provider => new SemaphoreSlim(2)); return services; }
2. 移除ScrapingService中的信号量操作
public class ScrapingService : IScrapingService { // 保留必要依赖,移除SemaphoreSlim注入 // .. properties and constructor here public async Task ScrapeAsync() { // 移除此处的WaitAsync和Release调用 // do your scraping stuff } }
3. 优化ConsoleHostedService的工作任务逻辑
internal sealed class ConsoleHostedService : IHostedService { private readonly IServiceProvider _serviceProvider; private readonly SemaphoreSlim _semaphore; private readonly ILogger<ConsoleHostedService> _logger; private readonly IHostApplicationLifetime _appLifetime; private readonly List<Task> _workerTasks = new(); public ConsoleHostedService(IServiceProvider serviceProvider, SemaphoreSlim semaphore, ILogger<ConsoleHostedService> logger, IHostApplicationLifetime appLifetime) { _serviceProvider = serviceProvider; _semaphore = semaphore; _logger = logger; _appLifetime = appLifetime; } public Task StartAsync(CancellationToken cancellationToken) { _appLifetime.ApplicationStarted.Register(() => { // 启动与信号量许可数一致的工作任务 for (int i = 0; i < 2; i++) { _workerTasks.Add(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, "抓取任务发生未处理异常"); } finally { _semaphore.Release(); } } }, cancellationToken)); } }); return Task.CompletedTask; } // 可选:实现StopAsync确保工作任务优雅停止 public async Task StopAsync(CancellationToken cancellationToken) { await Task.WhenAll(_workerTasks); } }
修复说明
- Singleton信号量:确保所有工作任务共享同一个并发控制实例,准确限制全局并发数。
- 移除双重等待:让ConsoleHostedService统一负责并发控制,避免许可被重复消耗。
- 固定数量工作任务:启动与信号量许可数相同的任务,每个任务循环执行抓取逻辑。当一个任务完成释放许可后,会立即进入下一次循环,始终保持最大并发数,不会出现空窗期。
内容的提问来源于stack exchange,提问作者Maik Hasler
相关产品推荐
相关产品推荐

