SemaphoreSlim 是 C# 限流最常用选择,因其轻量、异步友好、专为 await 设计,限制同时进入临界区的任务数而非 Task 总数;需在共享作用域初始化且初始计数非零,必须用 await WaitAsync() 和 try/finally 或 await using 确保 Release() 执行;常见错误包括释放次数不匹配、未 await、方法内新建实例;它适用于任意 I/O 异步操作,而 ParallelOptions.MaxDegreeOfParallelism 仅对 CPU 绑定同步循环有效。
SemaphoreSlim 是 C# 限流最常用的选择因为它是轻量、异步友好的信号量实现,专为 await 场景设计。相比 Monitor 或 lock,它不会阻塞线程;相比 Task.Run + 队列手动调度,它省去大量协调逻辑。关键点在于:它限制的是「同时进入临界区的任务数」,不是「已创建的 Task 总数」。
SemaphoreSlim 实现并发控制必须在共享作用域(如类字段)中初始化一次,且初始计数不能为 0(否则所有 WaitAsync() 都会挂起)。典型用法是包裹实际耗时操作,而非仅包裹 Task.Run。
new SemaphoreSlim(5) 表示最多 5 个任务可同时执行,第 6 个会等待前一个 Release()
await semaphore.WaitAsync() 而非 Wait(),否则可能死锁或线程饥饿Release() 总被执行,推荐用 try/finally 或 using(C# 12+ 支持 await using)private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(3);
public async Task
FetchDataAsync(string url) { await _semaphore.WaitAsync(); try { return await _httpClient.GetStringAsync(url); } finally { _semaphore.Release(); } }
最典型的错误是 Release() 调用次数多于 WaitAsync(),导致计数溢出,后续限流失效;或者忘记 await 导致同步阻塞;还有把 SemaphoreSlim 声明在方法内,每次调用都新建,完全不起限流作用。
semaphore.Release(2) 但只 WaitAsync() 了一次 → 计数变 4,下次允许 4 个并发semaphore.WaitAsync().GetAwaiter().GetResult() → 同步等待,UI 线程或 ASP.NET 同步上下文可能死锁var s = new SemaphoreSlim(1) → 每次调用都是新实例,无共享控制ParallelOptions.MaxDegreeOfParallelism 的区别在哪Parallel.ForEach 中的 MaxDegreeOfParallelism 只控制 Parallel 内部线程调度,不适用于 async/await 方法;而 SemaphoreSlim 是纯逻辑门控,对任何 Task 都有效,包括 HTTP 调用、数据库查询、文件读写等 I/O 异步操作。
Parallel.ForEach(..., new ParallelOptions { MaxDegreeOfParallelism = 4 }):仅对 CPU 绑定的同步循环生效SemaphoreSlim:能精准约束 HttpClient 并发请求数、EF Core SaveChangesAsync 并发数等真实 I/O 场景SemaphoreSlim,Parallel 在这里基本没用真正难的不是加一行 WaitAsync(),而是确认哪些操作确实该被纳入同一把锁——比如是否要把日志写入、缓存更新也计入并发配额,这取决于你的资源瓶颈点在哪。