为什么 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);
<p>public async Task<string> 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(),而是确认哪些操作确实该被纳入同一把锁——比如是否要把日志写入、缓存更新也计入并发配额,这取决于你的资源瓶颈点在哪。
