导读:本期聚焦于乐少创作的《C#多线程中Semaphore如何正确使用?一文讲透信号量的原理与实战技巧》,敬请观看详情。C#中的Semaphore是控制线程并发数量的重要工具,它允许指定个数的线程同时访问某一段代码或资源。本文从信号量的底层原理讲起,详细说明Semaphore和SemaphoreSlim的区别与各自的适用场景,通过完整的代码示例演示WaitOne和Release的配合使用,分析常见误区,比如Release次数超过初始计数导致的异常、忘记释放信号量造成的线程阻塞等问题,并给出资源池管理、限流控制等典型实战方案,帮助开发者在多线程编程中正确运用信号量。

C#多线程编程中,我们经常遇到这样的需求:不要求线程互斥,只要求同时访问某个资源的线程数量不能超过上限。比如下载器最多同时开5个连接、数据库连接池最多容纳10个连接。这时Lock和Monitor这类互斥手段就不合适了,Semaphore信号量才是对症的工具。它通过一个内部计数器来控制并发度,线程进入前计数减一,退出后计数加一,计数为零时后续线程只能等待。

C#多线程中Semaphore如何正确使用?一文讲透信号量的原理与实战技巧

Semaphore的基本原理与命名信号量

Semaphore的内部维护一个整数计数器,构造时需要传入两个参数:初始可用数量和最大并发数量。初始数量表示一开始有几个"许可证"可以发放,最大数量则限制了Release能恢复的上限。线程调用WaitOne时申请许可证,如果当前计数大于零则计数减一并立即返回,线程继续执行;如果计数已经为零,线程就进入阻塞队列等待,直到其他线程调用Release归还许可证。

值得注意的是,Semaphore的计数可以理解为"资源的剩余额度"而不是"已被占用的数量",这一点和SemaphoreSlim的使用没有区别,但初次接触时容易混淆方向。初始计数设为3,意味着最多同时允许3个线程通过,第4个线程必须等前面某个线程释放。

命名信号量是System.Threading.Semaphore的一个特殊能力,构造时传入名称后,信号量在整个操作系统范围内可见,可以跨进程协同工作。例如限制同一台机器上多个进程对某个文件的并发访问:

// 创建一个命名信号量,初始2个许可,最大2个,跨进程可见
using var sem = new Semaphore(2, 2, "Global\\MyFileSemaphore");
sem.WaitOne();
try
{
    Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} 获得许可,开始处理");
    Thread.Sleep(2000); // 模拟耗时操作
}
finally
{
    sem.Release(); // 必须在finally中释放,防止异常导致许可泄漏
}

名称前缀Global表示在全局命名空间创建,即使多个会话环境中的进程也能共享同一个信号量。如果不加前缀,信号量只在当前会话内可见。跨进程场景下要特别注意异常处理,因为Semaphore没有基于Process的自动释放机制,进程异常退出虽然会释放句柄,但已授予的许可在内核对象销毁前可能造成短暂的计数错乱,可借助构造时传入out createdNew参数做初始化保护。

Semaphore与SemaphoreSlim的区别及选型

.NET提供了两个信号量类型,很多初学者不清楚该用哪个。System.Threading.Semaphore是对Windows内核信号量对象的封装,功能强大但开销较高,每次WaitOne和Release都涉及内核态切换,同时它支持命名实例和跨进程同步。SemaphoreSlim则是.NET 4.0引入的轻量级托管实现,计数逻辑完全在用户态完成,只有在需要真正阻塞等待时才借助内核事件对象,性能明显更好。

选型的基本原则很清晰:绝大多数单进程内的并发控制场景都应该用SemaphoreSlim。只有在需要跨进程同步、或者需要信号量的WaitHandle参与WaitHandle.WaitAny等组合等待时,才考虑使用Semaphore。此外SemaphoreSlim提供了CancellationToken支持、WaitAsync异步等待方法以及AvailableWaitHandle属性,在async/await代码中它是唯一合理的选择,因为Semaphore的WaitOne是同步阻塞的,直接用在异步方法中会占用线程池线程,违背异步编程的初衷。

下面的例子演示SemaphoreSlim在异步限流中的典型用法,比如并发调用一个限流为3的外部接口:

var semaphore = new SemaphoreSlim(3, 3); // 最多3个并发

var tasks = Enumerable.Range(1, 10).Select(async i =>
{
    Console.WriteLine($"请求{i} 等待进入");
    await semaphore.WaitAsync(); // 异步等待,不阻塞线程
    try
    {
        Console.WriteLine($"请求{i} 开始执行");
        await Task.Delay(1000); // 模拟异步IO
        Console.WriteLine($"请求{i} 执行完毕");
    }
    finally
    {
        semaphore.Release();
    }
});

await Task.WhenAll(tasks);
Console.WriteLine($"剩余可用许可: {semaphore.CurrentCount}");

这段代码中10个异步请求争抢3个许可,输出会清晰地展示每次只有3个请求在同时执行。CurrentCount属性可以随时查看剩余许可数,方便做监控和日志。执行结果还体现出信号量不保证公平性,先等待的线程不一定先获得许可,这一点在编写对顺序有要求的逻辑时要留意。

常见误区与实战注意事项

第一类高频问题是Release调用次数超过最大上限。SemaphoreSlim的Release不接受超额释放,如果Release的次数使计数超过maximumCount,会直接抛出SemaphoreFullException。典型错误是在条件分支中只Release部分路径,或者循环里Wait一次却Release多次。规范做法是让Wait和Release严格成对出现,并始终把Release放在finally块中,这样即使try中抛出异常也能保证许可归还。

第二类问题是InitialCount和maximumCount的关系搞反。构造函数第一个参数是当前可用计数,第二个是最大值,初始值不能大于最大值。有人误以为第一个参数是最大并发数,写成了new Semaphore(5, 3),运行时立刻抛出ArgumentOutOfRangeException。建议写代码时始终用命名参数或注释明确含义,降低出错概率。

第三类问题出现在异步与同步混用上。SemaphoreSlim不能在持有许可的代码中再次调用WaitAsync去等待同一个信号量,若许可已耗尽会造成自我死锁。如果确实需要可重入语义,应该在业务层自己维护重入计数,而不是依赖信号量。另外SemaphoreSlim的WaitAsync虽然不阻塞线程,但等待的Task没有超时控制时可能长时间挂起,生产环境建议传入超时和取消令牌:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
if (await semaphore.WaitAsync(TimeSpan.FromSeconds(3), cts.Token))
{
    try
    {
        // 获得许可后的业务逻辑
    }
    finally
    {
        semaphore.Release();
    }
}
else
{
    Console.WriteLine("等待超时,未获得许可,走降级逻辑");
}

最后补充一个资源池的实战模式。信号量配合队列可以轻松实现对象池:信号量控制并发上限,ConcurrentQueue存放可用对象,获取时Wait成功后从队列取对象,用完归还时先把对象放回队列再Release。这个模式在数据库连接池、Socket连接池的实现中广泛使用。理解了信号量的计数本质和成对使用的纪律,它就能成为多线程限流与资源管理中最可靠的工具之一。

C#Semaphore多线程修改时间:2026-09-01 17:45:27

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。