CodeGym /课程 /C# SELF /信号量: Semaphore 和 <...

信号量: SemaphoreSemaphoreSlim

C# SELF
第 56 级 , 课程 3
可用

1. 介绍

Mutexlock 就像只服务一个客人的咖啡师。但如果我们不是只有一台咖啡机,而是有三台——可以同时做三杯咖啡怎么办?

比如,你有一家咖啡馆,有三台咖啡机。顾客(线程)来,占用空闲的机器,做完咖啡就离开。如果三台都被占了,其他人就得等到有一台空出来。

问题: 怎么保证最多只有三个顾客同时在咖啡机上,其他人排队等候?
答案:信号量

什么是信号量?

信号量是经典的同步工具。如果 lock/Mutex 控制“一个进去——其他人等”,信号量则说:“我允许同时有 N 个!”。

信号量由 Edsger Dijkstra 在 1965 年提出。名字来自海上信号:就像旗帜传递可用信息,信号量在代码里告诉线程——可以进来还是得等着。

使用场景

  • 限制同时访问某个资源的线程数量。
  • 限制并发连接到数据库的数量、并行请求数、重任务数等。

2. 类概览: SemaphoreSemaphoreSlim

Semaphore

  • 比较“重”,使用操作系统内核对象 (kernel objects)。
  • 支持跨进程的线程同步。
  • 可以指定名字并在进程间共享。

SemaphoreSlim

  • 精简版本,只在单个进程内工作。
  • 更快,资源消耗更少。
  • 如果不需要跨进程同步,几乎总是优先使用。

类比:轻便背包SemaphoreSlim)对比 大行李箱Semaphore)。旅行轻装就背背包。

对比表

跨进程 性能 推荐场景
Semaphore
慢一些 需要进程间同步时使用
SemaphoreSlim
更快 99% 的情况,单进程内使用

信号量的主要方法和属性

主要参数

  • InitialCount — 初始许可数。
  • MaxCount — 最多同时发放的许可数。

关键方法

  • Wait()WaitAsync() — 请求访问(占用一个许可)。
  • Release() — 释放一个许可。

工作原理
如果在调用 Wait() 时没有可用许可,线程会被阻塞并等待,直到有人调用 Release()。释放后,等待的某个线程会继续执行。

3. 第一个实践例子

在控制台程序里加一个“停车场”,只有 3 个车位,并尝试启动 10 个线程。

using System;
using System.Threading;

class Program
{
    // 信号量,有 3 个许可(3 个停车位)
    static SemaphoreSlim parking = new SemaphoreSlim(3);

    static void Main()
    {
        for (int i = 1; i <= 10; i++)
        {
            int carNumber = i;
            new Thread(() =>
            {
                Console.WriteLine($"机器车 #{carNumber} 尝试停车...");
                parking.Wait(); // 等待空位
                Console.WriteLine($"机器车 #{carNumber} 停进停车场了!");
                Thread.Sleep(2000); // 在停车场停 2 秒
                Console.WriteLine($"机器车 #{carNumber} 驶离停车场。");
                parking.Release(); // 释放车位
            }).Start();
        }
    }
}
  • 一次最多只有三辆“车”能停车。
  • 其他的会等到有空位为止。
  • 输出可能交错——这是多线程的正常现象。

4. 信号量作为负载限制器

把同时运行的重任务(比如下载)限制到 5 个。

static SemaphoreSlim semaphore = new SemaphoreSlim(5); // 最多 5 个并发下载

static void DownloadFile(int fileId)
{
    semaphore.Wait();
    try
    {
        Console.WriteLine($"--> 开始下载文件 {fileId}");
        Thread.Sleep(1000 + fileId * 100); // 下载(模拟)
        Console.WriteLine($"<-- 文件 {fileId} 下载完成");
    }
    finally
    {
        semaphore.Release();
    }
}

static void Main()
{
    for (int i = 1; i <= 12; i++)
    {
        int localId = i;
        new Thread(() => DownloadFile(localId)).Start();
    }
}

重要点:Wait() 放在 try 之前,把 Release() 放在 finally。这样即使发生异常,许可也一定会被释放。

5. Wait(int millisecondsTimeout) 和异步方法

可以只等待有限的时间:

if (semaphore.Wait(500))
{
    // 成功在半秒内拿到许可!
}
else
{
    // 500 ms 内没等到 — 放弃
}

在现代应用(比如 ASP.NET)里,应该使用异步版本:await semaphore.WaitAsync()。这不会在等待许可时阻塞执行线程。
注意:在异步代码里要用 SemaphoreSlim 和它的 WaitAsync,否则可能遇到不可预期的 deadlock。

6. 错误用法和正确用法示例

常见错误是忘记调用 Release():许可会“泄露”,程序会停住。

错误示例

static void SomeWork()
{
    semaphore.Wait();
    // ... 处理,但忘了 Release()!
}

正确示例

static void SomeWork()
{
    semaphore.Wait();
    try
    {
        // 处理
    }
    finally
    {
        semaphore.Release();
    }
}

异步版本

static async Task SomeAsyncWork()
{
    await semaphore.WaitAsync();
    try
    {
        // 异步处理
    }
    finally
    {
        semaphore.Release();
    }
}

7. 信号量的内部结构(通俗解释)

信号量就是一个计数器。Wait() 把它减 1。如果计数器 > 0 —— 线程通过;如果等于 0 —— 线程等待。Release() 增加计数并唤醒等待者。


+-------------------------------+
| 信号量 (计数 = 3)            |
+-------------------------------+
|  [ ]  [ ]  [ ]                | <--- 许可
+----+----+----+----------------+
     |    |    |
   线程 线程 线程

8. 有用的细节

与其他原语的区别

  • lock / Monitor / Mutex — 只允许一个线程(排他访问)。
  • Semaphore/SemaphoreSlim — 允许有限的 N 个线程同时通过。

信号量不绑定“所有者”:任何线程都可以释放许可。这是特性,不是 bug。

现实世界的应用

  • 限制到某个服务或数据库的并发连接数。
  • 资源池:资源上最多有 N 个线程。
  • 限制同时处理的 web 请求数。
  • 读/写限制以防止过载。
  • 限制对外部 API 的调用次数。

错误示例(Release 比 Wait 多)

var semaphore = new SemaphoreSlim(2);
semaphore.Release(); // 错误!计数变成 3,超过了 MaxCount — 会抛出 SemaphoreFullException.

这里会抛出 SemaphoreFullException:计数超过了最大值。

SemaphoreSemaphoreSlim 的区别

  • SemaphoreSlim — 进程内、速度快、用法简单(几乎总是用它)。
  • Semaphore — 在需要跨进程同步时才用(罕见场景)。

为什么要了解信号量?

面试中常见的问题:“怎么限制同时访问某个资源的线程数?”——正确答案就是:信号量

  • lock — 1 个线程。
  • Semaphore/SemaphoreSlimN 个线程。

9. 常见错误和使用注意事项

错误 №1: 忘记调用 Release(). 如果线程占用了许可(通过 Wait()WaitAsync()),却没有释放,其他线程会无限等待——应用会“卡住”。

错误 №2: 调用 Release() 的次数多于 Wait() 的次数. 会出现“多余”的许可。对于 Semaphore 这会导致 SemaphoreFullException,破坏访问逻辑。

错误 №3: 混用不同的同步机制. 一处用 lock,另一处用信号量去保护同一资源,会增加死锁的风险。

错误 №4: 在异步代码里用 Semaphore. 经典的 Semaphore 不太适合 async/await。异步场景要用 SemaphoreSlimWaitAsync()

错误 №5: 错误设置 initialCountmaxCount. 值设置不当可能被绕过,导致比预期更多的线程访问资源。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION