CodeGym /課程 /C# SELF /信號量: Semaphore 和 <...

信號量: SemaphoreSemaphoreSlim

C# SELF
等級 56 , 課堂 3
開放

1. 介紹

Mutexlock 就像咖啡師一次只服務一位客人。但如果不是只有一台咖啡機,而是三台可以同時煮三杯咖啡呢?

例如,你有一家咖啡廳,三台咖啡機。客人(執行緒)來,佔用空的機器,做完咖啡就離開。如果三台都被佔滿,其他人就要等到有一台釋出。

問題: 如何確保同一時間最多只有三位客人在咖啡機前工作,其他人排隊等候?
答案: 使用 信號量

什麼是信號量?

信號量是經典的同步工具。如果 lock/Mutex 控制「一個進去,其他等候」,那信號量會說:「我允許 N 個同時進來!」。

信號量由 Edsger Dijkstra 在 1965 年提出。名稱來自海上信號:像旗子傳達能否通行一樣,信號量告訴執行緒要進入還是等待。

應用場景

  • 限制同時存取某個資源的執行緒數量。
  • 限制同時到資料庫的連線數、並行請求數或耗資源的工作數量。

2. 類別概覽: SemaphoreSemaphoreSlim

Semaphore

  • 重量級類別,使用作業系統核心物件 (kernel objects)。
  • 支援跨程序(process)之間的同步。
  • 可以指定名稱並在不同程序間共享。

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 個執行緒同時進入。

信號量不綁定「擁有者」:任何執行緒都可以釋放許可。這是設計使然,不是錯誤。

實際應用

  • 限制到服務或資料庫的平行連線數。
  • 資源池:最多有 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,另一些地方用信號量去保護同一資源,會提升 mutual deadlock 的風險。

錯誤 №4: 在非同步程式中使用 Semaphore 傳統的 Semaphore 不太適合 async/await。非同步場景請使用 SemaphoreSlim 及其 WaitAsync()

錯誤 №5: 錯誤設定 initialCountmaxCount 值設定不當可能讓限制失效,導致比預期更多的執行緒同時訪問資源。

2
任務
C# SELF, 等級 56, 課堂 3
上鎖
請建立一個主控台應用程式,模擬一個有 2 個可用停車位的停車場運作。
請建立一個主控台應用程式,模擬一個有 2 個可用停車位的停車場運作。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION