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(); } }
常见踩坑:释放次数不匹配、未 await、跨作用域复用
最典型的错误是 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(),而是确认哪些操作确实该被纳入同一把锁——比如是否要把日志写入、缓存更新也计入并发配额,这取决于你的资源瓶颈点在哪。










