首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >.NET 异步并发:彻底搞懂 SemaphoreSlim

.NET 异步并发:彻底搞懂 SemaphoreSlim

原创
作者头像
步步为营DotNet
发布2026-08-30 16:39:24
发布2026-08-30 16:39:24
390
举报

在 .NET 开发中,经常会遇到这样的需求:

我有很多任务需要执行,但是不能让它们全部同时执行,最多只能同时执行 N 个。

例如:

  • 批量处理文件
  • 批量下载文件
  • 批量调用 HTTP 接口
  • 批量处理图片
  • 批量执行数据库操作
  • 批量调用 AI 接口

如果任务数量比较少,可以直接:

代码语言:javascript
复制
var tasks = items.Select(item => ProcessAsync(item));

await Task.WhenAll(tasks);

但是如果有 1000 个甚至 10000 个任务,就不能让它们全部同时执行。

这时候就需要:

代码语言:javascript
复制
SemaphoreSlim

它是 .NET 异步并发编程中非常重要的一个工具。

本文不讨论复杂的并发理论,只通过几个简单的例子,彻底搞懂:

SemaphoreSlim 到底是什么、为什么需要它、WaitAsync 和 Release 是什么,以及它为什么特别适合异步代码。


一、先从一个问题开始:任务太多怎么办?

假设我们有 100 个文件:

代码语言:javascript
复制
var files = GetFiles();

现在需要逐个处理。

最简单的方式:

代码语言:javascript
复制
foreach (var file in files)
{
    await ProcessAsync(file);
}

这种方式是:

代码语言:javascript
复制
文件1
 ↓
完成

文件2
 ↓
完成

文件3
 ↓
完成

文件4
 ↓
完成

同一时间只有一个文件处理。

这叫:

串行执行


二、为什么要并发?

假设:

代码语言:javascript
复制
每个文件处理需要 2 秒

100 个文件串行执行:

代码语言:javascript
复制
100 × 2 = 200 秒

如果可以同时处理 5 个:

代码语言:javascript
复制
100 ÷ 5 × 2 ≈ 40 秒

所以我们可能会想到:

代码语言:javascript
复制
var tasks = files.Select(file => ProcessAsync(file));

await Task.WhenAll(tasks);

这样多个文件就可以同时处理。

例如:

代码语言:javascript
复制
文件1 ───────→ 完成
文件2 ─────────→ 完成
文件3 ─────→ 完成
文件4 ───────────→ 完成
文件5 ───────→ 完成

效率提高了。

但是问题来了。


三、并发是不是越多越好?

不是。

假设有:

代码语言:javascript
复制
10000 个文件

如果直接:

代码语言:javascript
复制
var tasks = files.Select(file => ProcessAsync(file));

await Task.WhenAll(tasks);

那么可能会产生大量并发任务。

具体能不能承受,要看你的任务到底是什么。

例如文件处理可能同时占用:

代码语言:javascript
复制
CPU
内存
磁盘 I/O
文件句柄
第三方库资源

如果同时处理几千个文件,很可能导致:

代码语言:javascript
复制
CPU过高
内存占用过高
磁盘压力过大
程序变慢
甚至程序异常

所以真正合理的需求通常不是:

尽可能多地并发。

而是:

限制并发数量。

例如:

代码语言:javascript
复制
10000 个文件

最多同时处理:
10 个

这就是 SemaphoreSlim 的核心用途。


四、SemaphoreSlim 是什么?

可以把 SemaphoreSlim 理解成:

一个“有限数量的通行证”。

例如:

代码语言:javascript
复制
var semaphore = new SemaphoreSlim(3);

相当于现在有:

代码语言:javascript
复制
🎫 🎫 🎫

一共 3 张通行证。

一个任务想要执行某个操作:

必须先拿到一张通行证。

拿到以后:

代码语言:javascript
复制
任务1 → 🎫
任务2 → 🎫
任务3 → 🎫

3 张票全部被拿走。

这时候任务4来了:

代码语言:javascript
复制
任务4 → 没票 → 等待

等任务1执行完成,把票还回来:

代码语言:javascript
复制
任务1
 ↓
Release()
 ↓
归还 🎫

任务4就可以拿到票:

代码语言:javascript
复制
任务4 → 🎫

所以最终就实现了:

最多 3 个任务同时执行。


五、SemaphoreSlim 最核心的两个方法

理解 SemaphoreSlim,最重要的是两个方法:

代码语言:javascript
复制
WaitAsync()

和:

代码语言:javascript
复制
Release()

可以直接记成:

代码语言:javascript
复制
WaitAsync()
    ↓
拿一张票

Release()
    ↓
还一张票

六、WaitAsync:获取许可证

例如:

代码语言:javascript
复制
var semaphore = new SemaphoreSlim(3);

await semaphore.WaitAsync();

意思就是:

我要进入,但是我要先获取一个许可证。

如果还有许可证:

代码语言:javascript
复制
🎫 🎫 🎫

任务1 → 🎫

剩下:

代码语言:javascript
复制
🎫 🎫

任务2:

代码语言:javascript
复制
任务2 → 🎫

剩下:

代码语言:javascript
复制
🎫

任务3:

代码语言:javascript
复制
任务3 → 🎫

现在:

代码语言:javascript
复制
没有许可证了

任务4:

代码语言:javascript
复制
await semaphore.WaitAsync();

就会等待。


七、等待并不会一直占用线程

这是 SemaphoreSlim 非常重要的一点。

看到:

代码语言:javascript
复制
await semaphore.WaitAsync();

很多人会误以为:

“是不是有一个线程卡在那里?”

不是。

这是:

异步等待。

简单理解:

代码语言:javascript
复制
同步阻塞:

线程
 ↓
等待
 ↓
线程一直被占着

而:

代码语言:javascript
复制
异步等待:

Task
 ↓
等待
 ↓
不需要线程一直傻等
 ↓
条件满足
 ↓
继续执行

因此:

代码语言:javascript
复制
await semaphore.WaitAsync();

非常适合放在异步方法中。


八、Release:归还许可证

任务执行完成以后:

代码语言:javascript
复制
semaphore.Release();

表示:

我已经完成了,把许可证还回去。

例如:

代码语言:javascript
复制
SemaphoreSlim(3)

任务1 → 处理中
任务2 → 处理中
任务3 → 处理中

任务4 → 等待

任务1完成:

代码语言:javascript
复制
任务1
 ↓
Release()
 ↓
归还许可证

于是:

代码语言:javascript
复制
任务2 → 处理中
任务3 → 处理中
任务4 → 处理中

任务4获得了执行机会。


九、为什么一定要 try/finally?

这是实际项目中使用 SemaphoreSlim 最重要的代码规范之一。

不要这样写:

代码语言:javascript
复制
await semaphore.WaitAsync();

await ProcessAsync();

semaphore.Release();

因为:

代码语言:javascript
复制
await ProcessAsync();

可能发生异常。

例如:

代码语言:javascript
复制
WaitAsync()
    ↓
成功拿到许可证
    ↓
ProcessAsync()
    ↓
发生异常
    ↓
程序跳出
    ↓
Release() 没有执行

那么许可证就丢了。

如果这种情况发生很多次,最终可能导致:

代码语言:javascript
复制
许可证越来越少
        ↓
越来越多任务等待
        ↓
程序看起来像“卡死”

因此标准写法是:

代码语言:javascript
复制
await semaphore.WaitAsync();

try
{
    await ProcessAsync();
}
finally
{
    semaphore.Release();
}

这样无论:

代码语言:javascript
复制
正常完成
异常
取消

最终都会执行:

代码语言:javascript
复制
semaphore.Release();

十、CancellationToken 应该怎么配合?

实际项目中通常还要支持取消操作。

例如:

代码语言:javascript
复制
await semaphore.WaitAsync(ct);

这里的:

代码语言:javascript
复制
ct

就是 CancellationToken

如果用户点击“取消”:

代码语言:javascript
复制
CancellationToken
       ↓
WaitAsync(ct)
       ↓
等待中的任务取消等待

因此推荐:

代码语言:javascript
复制
await semaphore.WaitAsync(ct);

try
{
    ct.ThrowIfCancellationRequested();

    await ProcessAsync(ct);
}
finally
{
    semaphore.Release();
}

这里有一个非常细节但重要的问题。


十一、为什么 WaitAsync 放在 try 外面?

看:

代码语言:javascript
复制
await semaphore.WaitAsync(ct);

try
{
    ...
}
finally
{
    semaphore.Release();
}

而不是:

代码语言:javascript
复制
try
{
    await semaphore.WaitAsync(ct);
    ...
}
finally
{
    semaphore.Release();
}

原因是:

只有成功拿到许可证以后,才能 Release。

如果:

代码语言:javascript
复制
await semaphore.WaitAsync(ct);

还没拿到许可证,就因为取消抛出了:

代码语言:javascript
复制
OperationCanceledException

那么你根本没有获得许可证。

这时候不能:

代码语言:javascript
复制
semaphore.Release();

否则许可证数量就会被错误增加。

因此:

代码语言:javascript
复制
await semaphore.WaitAsync(ct);

try
{
    ...
}
finally
{
    semaphore.Release();
}

是非常经典的写法。


十二、完整的 SemaphoreSlim 示例

假设:

代码语言:javascript
复制
100 个任务
最多同时执行 3 个

可以写:

代码语言:javascript
复制
using var semaphore = new SemaphoreSlim(3);

var tasks = items.Select(async item =>
{
    await semaphore.WaitAsync();

    try
    {
        await ProcessAsync(item);
    }
    finally
    {
        semaphore.Release();
    }
});

await Task.WhenAll(tasks);

这里有三个关键点。

1. 创建许可证

代码语言:javascript
复制
new SemaphoreSlim(3)

表示:

最多允许 3 个任务同时进入。

2. 获取许可证

代码语言:javascript
复制
await semaphore.WaitAsync();

表示:

我要执行,先拿许可证。

3. 归还许可证

代码语言:javascript
复制
semaphore.Release();

表示:

我执行完了,可以让下一个任务进来。


十三、Task.WhenAll 和 SemaphoreSlim 的区别

这两个东西经常一起出现,但职责完全不同。

Task.WhenAll

代码语言:javascript
复制
await Task.WhenAll(tasks);

负责:

等待所有任务完成。


SemaphoreSlim

代码语言:javascript
复制
await semaphore.WaitAsync();

负责:

限制同时执行的任务数量。

所以:

代码语言:javascript
复制
Task.WhenAll
    ↓
什么时候全部结束?

SemaphoreSlim
    ↓
同时允许多少个?

这是两个完全不同的问题。


十四、为什么 Select 可以创建很多 Task?

例如:

代码语言:javascript
复制
var tasks = items.Select(async item =>
{
    await semaphore.WaitAsync();

    try
    {
        await ProcessAsync(item);
    }
    finally
    {
        semaphore.Release();
    }
});

假设:

代码语言:javascript
复制
items = 1000 个

那么可以理解成:

代码语言:javascript
复制
Task1
Task2
Task3
...
Task1000

但是:

并不是 1000 个任务同时进入 ProcessAsync

因为前面有:

代码语言:javascript
复制
await semaphore.WaitAsync();

如果:

代码语言:javascript
复制
new SemaphoreSlim(5)

那么:

代码语言:javascript
复制
1000个Task
      ↓
SemaphoreSlim(5)
      ↓
最多5个进入
      ↓
995个等待

所以:

创建多少 Task 和同时执行多少任务,是两个不同的问题。


十五、SemaphoreSlim 控制的是并发,不是线程

这一点一定要记住。

例如:

代码语言:javascript
复制
new SemaphoreSlim(5)

不是:

创建 5 个线程。

而是:

最多允许 5 个任务同时通过。

所以:

代码语言:javascript
复制
Task ≠ Thread

尤其是异步 I/O 场景:

代码语言:javascript
复制
await httpClient.GetAsync(...);

await stream.ReadAsync(...);

await stream.WriteAsync(...);

任务等待 I/O 时,并不意味着一个线程一直在那里等待。

因此:

代码语言:javascript
复制
SemaphoreSlim

特别适合:

异步任务的并发限制。


十六、SemaphoreSlim 和 lock 有什么区别?

这也是非常容易混淆的地方。

lock

代码语言:javascript
复制
lock (obj)
{
    // 临界区
}

表达的是:

同一时间只能有一个线程执行这里。


SemaphoreSlim

代码语言:javascript
复制
var semaphore = new SemaphoreSlim(5);

表达的是:

同一时间最多允许 5 个任务执行这里。

因此:

代码语言:javascript
复制
lock
 ↓
最多1个

SemaphoreSlim(5)
 ↓
最多5个

而且 SemaphoreSlim 支持:

代码语言:javascript
复制
await semaphore.WaitAsync();

因此更适合异步场景。


十七、SemaphoreSlim(1) 是什么?

如果:

代码语言:javascript
复制
var semaphore = new SemaphoreSlim(1);

那么就相当于:

代码语言:javascript
复制
🎫

只有一张票。

所以:

代码语言:javascript
复制
Task1 → 执行
Task2 → 等待
Task3 → 等待

Task1完成:

代码语言:javascript
复制
Task1
 ↓
Release()
 ↓
Task2执行

因此:

代码语言:javascript
复制
SemaphoreSlim(1)

可以用来实现一种:

异步环境下的互斥访问。

不过如果只是为了保护一个简单变量,不应该为了这个目的专门使用 SemaphoreSlim,这时候可能 Interlocked 更合适。


十八、SemaphoreSlim 和 Semaphore 有什么区别?

.NET 中还有一个:

代码语言:javascript
复制
Semaphore

它和:

代码语言:javascript
复制
SemaphoreSlim

名字非常像。

简单理解:

Semaphore

偏向于:

跨进程同步。

SemaphoreSlim

偏向于:

当前进程内部的并发控制。

而且 SemaphoreSlim 提供了:

代码语言:javascript
复制
WaitAsync()

因此在现代 .NET 异步代码中:

如果只是控制当前应用程序内部的异步并发,通常优先考虑 SemaphoreSlim


十九、实际项目:批量文件处理

现在回到最开始的场景。

例如你有:

代码语言:javascript
复制
targets

里面有 100 个文件。

你希望:

代码语言:javascript
复制
MaxParallel = 3

那么:

代码语言:javascript
复制
using var gate = new SemaphoreSlim(MaxParallel);

就是:

代码语言:javascript
复制
100 个文件任务

        ↓

SemaphoreSlim(3)

        ↓

最多3个文件同时改写

核心代码:

代码语言:javascript
复制
var tasks = targets.Select(async item =>
{
    await gate.WaitAsync(ct);

    try
    {
        ct.ThrowIfCancellationRequested();

        item.RewriteState = RewriteState.处理中;

        var result = await RewriteByExtensionAsync(
            sourcePath,
            destPath,
            ct,
            EnableDiffHighlight);

        item.RewriteState =
            result.IsOK
                ? RewriteState.已完成
                : RewriteState.错误;
    }
    finally
    {
        gate.Release();
    }
});

await Task.WhenAll(tasks);

运行过程可以理解成:

代码语言:javascript
复制
                100个文件
                    │
                    ▼
              创建100个Task
                    │
                    ▼
           SemaphoreSlim(3)
                    │
          ┌─────────┼─────────┐
          ▼         ▼         ▼
        文件1      文件2      文件3
        处理中     处理中     处理中
          │         │         │
          └─────────┼─────────┘
                    ▼
                 完成
                    │
                    ▼
               Release()
                    │
                    ▼
                文件4进入

这是代码最核心的设计。


原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 在 .NET 开发中,经常会遇到这样的需求:
  • 一、先从一个问题开始:任务太多怎么办?
  • 二、为什么要并发?
  • 三、并发是不是越多越好?
  • 四、SemaphoreSlim 是什么?
  • 五、SemaphoreSlim 最核心的两个方法
  • 六、WaitAsync:获取许可证
  • 七、等待并不会一直占用线程
  • 八、Release:归还许可证
  • 九、为什么一定要 try/finally?
  • 十、CancellationToken 应该怎么配合?
  • 十一、为什么 WaitAsync 放在 try 外面?
  • 十二、完整的 SemaphoreSlim 示例
    • 1. 创建许可证
    • 2. 获取许可证
    • 3. 归还许可证
  • 十三、Task.WhenAll 和 SemaphoreSlim 的区别
    • Task.WhenAll
    • SemaphoreSlim
  • 十四、为什么 Select 可以创建很多 Task?
  • 十五、SemaphoreSlim 控制的是并发,不是线程
  • 十六、SemaphoreSlim 和 lock 有什么区别?
    • lock
    • SemaphoreSlim
  • 十七、SemaphoreSlim(1) 是什么?
  • 十八、SemaphoreSlim 和 Semaphore 有什么区别?
    • Semaphore
    • SemaphoreSlim
  • 十九、实际项目:批量文件处理
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档