c# 如何限制并发任务的数量 c# SemaphoreSlim限流

来源:这里教程网 时间:2026-02-21 17:39:46 作者:

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

相关推荐