C# Semaphore与SemaphoreSlim限制多线程资源池访问实战
在C#多线程开发中,限制同时访问某一资源的线程数量是常见的并发控制需求。System.Threading.Semaphore和System.Threading.SemaphoreSlim是两个核心类,它们通过信号量机制管理资源池的并发访问量。
Semaphore 类的构造函数与常用 API
Semaphore 是一个内核级对象(Kernel-time),支持跨进程同步,适用于需要严格控制且可能涉及多进程的场景。其使用相对较重,但功能强大。
构造函数
Semaphore(Int32, Int32): 初始化实例,指定初始入口数和最大并发入口数。Semaphore(Int32, Int32, String): 上述基础上,指定系统信号灯对象的名称,可用于跨进程同步。Semaphore(Int32, Int32, String, Boolean): 进一步指定一个布尔变量,接收是否创建了新系统信号量的指示。
常用方法
在使用 Semaphore 时,主要依赖以下方法进行状态控制和资源释放:
WaitOne()/WaitOne(Int32)/WaitOne(TimeSpan): 阻止当前线程,直到获得进入信号量的权限。可指定超时时间。Release()/Release(Int32): 退出信号量,并可选择性地增加计数(即增加可用资源数)。OpenExisting(String)/TryOpenExisting(String, Semaphore): 打开指定名称的系统信号量(如果存在)。Close(): 释放由当前 WaitHandle 占用的所有资源。使用后建议调用此方法以释放内核对象。
使用 Semaphore 限制资源池示例
下面通过一个具体案例演示:启动10个线程执行计算任务,但同一时间只允许最多3个线程进入资源池执行。
第1步:实例化 Semaphore 并设置权限
首先创建一个 Semaphore 对象。构造函数第一个参数为initialCount(初始可用数),第二个为maximumCount(最大并发数)。
在以下示例中,我们将初始值设为0,意味着初始状态下没有任何线程可以进入。随后通过 _pool.Release(3) 手动放通3个名额,模拟“按钮开启”的效果。
class Program
{
private static int sum = 0;
private static Semaphore _pool;
private static int isComplete = 0;
static void Main(string[] args)
{
Console.WriteLine("执行程序");
// 初始允许0个进入,最大允许3个同时进入
_pool = new Semaphore(0, 3);
for (int i = 0; i < 10; i++)
{
Thread thread = new Thread(new ParameterizedThreadStart(AddOne));
thread.Start(i + 1);
}
Console.WriteLine("按任意键放通资源池...");
Console.ReadKey();
// 手动放通3个名额
_pool.Release(3);
// 等待所有线程完成任务
while (isComplete < 10)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
}
Console.WriteLine($"最终求和结果: {sum}");
_pool.Close(); // 释放内核资源
}
public static void AddOne(object n)
{
int id = (int)n;
Console.WriteLine($"线程{id}启动,等待进入资源池...");
// 阻塞直到获得进入权限
_pool.WaitOne();
Console.WriteLine($"线程{id}已进入资源池");
try
{
// 模拟资源占用和操作
for (int i = 0; i < 10; i++)
{
Interlocked.Add(ref sum, 1);
Thread.Sleep(TimeSpan.FromMilliseconds(500));
}
}
finally
{
// 必须释放,否则其他线程将永久阻塞
_pool.Release();
isComplete++;
Console.WriteLine($"线程{id}退出资源池");
}
}
}
代码逻辑解析
- 等待获取权限:
_pool.WaitOne()调用后,如果资源池已满(计数为0),线程会被挂起。当有其他线程调用Release()释放资源时,等待队列中的线程将被唤醒并获得计数。 - 释放资源:
_pool.Release()将计数加1。注意,这里必须在业务逻辑结束后(通常在finally块中)调用,以防止因异常导致资源不释放而引发死锁。
SemaphoreSlim 轻量级替代方案
在大多数 .NET 应用开发中,我们只需要在同一进程内进行线程同步。SemaphoreSlim` 是 `Semaphore` 的轻量级替代者,专为高性能设计。
主要特点
- 进程内同步: 不支持跨进程通信,因此性能优于 Semaphore。
- 异步友好: 提供
WaitAsync()` 和ReleaseAsync()` 方法,避免阻塞线程池线程,更适合 I/O 密集型或高并发 Web 场景。 - 无需 Close: 它是托管类型,垃圾回收会自动处理内存释放,通常不需要显式调用关闭方法。
构造函数
SemaphoreSlim(Int32): 仅指定初始可用数(默认最大等于初始数)。SemaphoreSlim(Int32, Int32): 指定初始可用数和最大并发数。
改造后的示例
将上述代码替换为 SemaphoreSlim,核心逻辑几乎不变,但 API 略有不同:
class Program
{
private static int sum = 0;
private static SemaphoreSlim _pool;
private static int isComplete = 0;
static async Task Main(string[] args)
{
Console.WriteLine("执行程序 (SemaphoreSlim)");
// 初始0,最大3
_pool = new SemaphoreSlim(0, 3);
var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
tasks.Add(Task.Run(() => AddOne(i + 1)));
}
Console.WriteLine("按任意键放通资源池...");
Console.ReadKey();
// 放通3个名额
_pool.Release(3);
await Task.WhenAll(tasks);
Console.WriteLine($"最终求和结果: {sum}");
}
public static async Task AddOne(int id)
{
Console.WriteLine($"线程{id}启动...");
// 等待进入权限
await _pool.WaitAsync();
Console.WriteLine($"线程{id}已进入");
try
{
for (int i = 0; i < 10; i++)
{
Interlocked.Add(ref sum, 1);
await Task.Delay(500);
}
}
finally
{
// 释放权限
_pool.Release();
Console.WriteLine($"线程{id}退出");
}
}
}Semaphore 与 SemaphoreSlim 的核心区别
| 特性 | Semaphore | SemaphoreSlim |
|---|---|---|
| 同步范围 | 支持跨进程(需命名) | 仅限进程内 |
| 底层实现 | 操作系统内核对象 | 纯 .NET 托管代码(自旋锁等) |
| 性能 | 较轻量级,但有上下文切换开销 | 更高效,适合高频调用 |
| 严格性监控 | 严格追踪 Enter/Exit 配对,错误调用会抛异常 | 不强制检查调用方标识,较宽松 |
| 异步支持 | 原生不支持,需配合 Task 包装 | 原生支持 WaitAsync / ReleaseAsync |
注意事项
- Count 准确性: 对于
SemaphoreSlim,如果未正确成对调用 Wait/Release,CurrentCount可能会变得不准确(甚至超过最大值)。编程人员需确保逻辑严密。 - 异常处理: 无论使用哪个类,释放资源的操作务必放在
finally块中,或者使用await using(如果有实现IDisposable的最佳实践,虽本例未展示Disposable模式的标准用法,但原则一致)。
总结
Semaphore 和 SemaphoreSlim 都是解决 C# 多线程资源竞争的有效手段。如果需要跨进程限制并发或需要极高的系统级隔离,选择 Semaphore;而在绝大多数常规的多线程和高并发应用开发中,推荐使用更轻量、支持异步操作的 SemaphoreSlim。
以上就是C# Semaphore与SemaphoreSlim限制多线程资源池访问的详细内容,更多关于C#并发编程的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









