在c#的并发编程场景中,当多个线程或异步任务需要访问有限的共享资源时,直接无限制地并发执行很容易导致资源耗尽、数据不一致等问题,SemaphoreSlim就是专门用来控制并发访问数量的同步原语,它比传统的Semaphore更轻量,更适合异步场景使用。

SemaphoreSlim基础使用方式
同步场景下的并发限制
如果是同步执行的任务,我们可以通过Wait方法获取信号量,任务完成后通过Release方法释放信号量,下面的示例限制最多同时有3个任务执行。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
// 初始化SemaphoreSlim,初始可用信号量为3,最大信号量也为3
static SemaphoreSlim semaphore = new SemaphoreSlim(3, 3);
static void Main()
{
// 启动10个同步任务
for (int i = 0; i < 10; i++)
{
int taskId = i;
Task.Run(() => ProcessTask(taskId));
}
Console.ReadLine();
}
static void ProcessTask(int taskId)
{
Console.WriteLine($"任务{taskId}等待获取信号量");
// 获取信号量,没有可用信号量时会阻塞当前线程
semaphore.Wait();
try
{
Console.WriteLine($"任务{taskId}开始执行,当前时间:{DateTime.Now:HH:mm:ss}");
// 模拟任务执行耗时
Thread.Sleep(2000);
Console.WriteLine($"任务{taskId}执行完成");
}
finally
{
// 释放信号量,让其他等待的任务可以获取
semaphore.Release();
}
}
}
异步场景下的并发限制
在异步编程中,使用同步的Wait方法可能会导致线程池阻塞,这时候应该使用WaitAsync方法,它不会阻塞线程,而是返回可等待的任务。
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static SemaphoreSlim semaphore = new SemaphoreSlim(2, 2);
static async Task Main()
{
// 启动8个异步任务
var tasks = new Task[8];
for (int i = 0; i < 8; i++)
{
int taskId = i;
tasks[i] = ProcessAsyncTask(taskId);
}
await Task.WhenAll(tasks);
}
static async Task ProcessAsyncTask(int taskId)
{
Console.WriteLine($"异步任务{taskId}等待获取信号量");
// 异步获取信号量,不会阻塞线程
await semaphore.WaitAsync();
try
{
Console.WriteLine($"异步任务{taskId}开始执行,当前时间:{DateTime.Now:HH:mm:ss}");
// 模拟异步操作耗时
await Task.Delay(1500);
Console.WriteLine($"异步任务{taskId}执行完成");
}
finally
{
semaphore.Release();
}
}
}
SemaphoreSlim核心属性与方法
我们可以通过下表了解SemaphoreSlim的核心成员:
| 成员名称 | 类型 | 说明 |
|---|---|---|
| CurrentCount | 属性 | 获取当前可用的信号量数量,也就是还可以同时允许多少个任务进入临界区 |
| Wait() | 方法 | 同步等待获取信号量,没有可用信号量时阻塞当前线程 |
| WaitAsync() | 方法 | 异步等待获取信号量,不会阻塞线程,支持传入取消令牌 |
| Release() | 方法 | 释放一个信号量,增加可用信号量数量,返回释放前的可用数量 |
| Dispose() | 方法 | 释放SemaphoreSlim占用的资源,使用后建议调用 |
SemaphoreSlim底层原理深入解析
SemaphoreSlim的设计目标是轻量且适配异步场景,它的底层实现和传统的Semaphore有本质区别,没有依赖操作系统的内核对象,而是完全基于托管代码实现。
核心数据结构
SemaphoreSlim内部主要维护以下几个核心部分:
- 一个int类型的计数器,记录当前可用的信号量数量,初始值由构造函数传入的初始信号量参数决定。
- 一个
ConcurrentQueue<TaskCompletionSource<bool>>队列,用来存储等待获取信号量的异步任务对应的等待源,当有信号量释放时,会从队列中取出等待源并设置结果,唤醒对应的异步任务。 - 同步等待的场景下,会结合
ManualResetEventSlim或者自旋等待的方式处理,避免直接阻塞线程池线程。
Wait与Release的执行逻辑
当调用Wait方法时,首先会原子性地减少可用信号量计数器,如果减少后计数器的值大于等于0,说明成功获取到信号量,方法直接返回。如果计数器小于0,说明没有可用信号量,当前线程会进入等待状态,直到其他任务调用Release方法释放信号量。
当调用WaitAsync方法时,同样先尝试原子性减少计数器,成功则直接返回完成的任务。如果失败,会创建一个TaskCompletionSource<bool>对象,放入内部的等待队列中,然后返回这个等待源对应的任务,外部通过await等待这个任务,不会阻塞线程。
调用Release方法时,会原子性地增加可用信号量计数器,然后检查内部的等待队列是否有等待的任务,如果有,就从队列中取出一个等待源,设置其结果为true,对应的异步任务就会被唤醒继续执行。如果是同步等待的场景,会唤醒一个等待的线程。
与Semaphore的区别
传统的Semaphore是内核级别的同步原语,依赖操作系统的内核对象,跨进程可用,但是创建和使用的开销更大。而SemaphoreSlim是用户态的实现,开销更小,速度更快,但是只能在同一进程内使用,不支持跨进程同步。如果不需要跨进程的场景,优先选择SemaphoreSlim。
使用注意事项
- 获取信号量和释放信号量必须成对出现,最好放在try-finally块中,避免因为异常导致信号量没有释放,造成死锁。
- 初始化时的最大信号量参数不能小于初始信号量参数,否则会抛出异常。
- 异步场景下不要混用
Wait和WaitAsync,避免不必要的线程阻塞。 - 使用完成后建议调用
Dispose方法释放资源,尤其是长期运行的应用中。
需要注意的是,SemaphoreSlim限制的是获取信号量的任务数量,而不是线程数量,在异步场景中,一个线程可以执行多个异步任务,所以SemaphoreSlim更适合控制异步任务的并发数量。
通过上面的内容,我们不仅掌握了SemaphoreSlim的基础用法,还了解了它的底层实现逻辑,在实际开发中可以根据场景合理选择使用方式,更好地处理并发访问的问题。
SemaphoreSlimcsharp并发控制异步编程修改时间:2026-07-03 07:18:31