并發編程 - 線程同步(九)之信號量Semaphore
前面對自旋鎖SpinLock進行了詳細學習,今天我們將學習另一個種同步機制——信號量Semaphore。

01、信號量是什么?
在 C# 中,信號量(Semaphore)是一種用于線程同步的機制,能夠控制對共享資源的訪問。它的工作原理是通過維護一個計數器來控制對資源的訪問次數。它常用于限制對共享資源(如數據庫連接池、文件系統、網絡資源等)的并發訪問。
1、信號量有三個核心概念:
1.計數:信號量的核心是一個計數器,表示當前資源的可用數量;
2.等待:當線程請求資源時,此次如果計數器大于0,則線程可以繼續執行,同時計數器減1;如果計數器等于0,則線程被阻塞直至其他線程釋放資源,即有線程增加計數器的值;
3.釋放:當線程使用完資源后,則需要釋放信號量,同時計數器加1,并喚醒其他等待的線程;
相信理解了信號量核心概念,其工作原理就不言而喻。
2、應用場景:
通過對信號量的工作原理了解,我們可以總結為:信號量就是為了控制對共享資源的訪問,保證共享資源不會被過度使用,因此可以引申出以下適用于信號量的場景:
1.控制各種連接池:限制同時打開各種資源的連接數量(比如連接打印機數量,數據庫連接數據,文件訪問數量);
2.限制網絡請求:防止服務器過載,導致服務器崩潰;
3.協調多個線程的執行順序:通過信號量控制生成者和消費者之間的資源訪問, 實現生產者和消費者模型;
02、C#中的信號量實現
C#提供了兩種信號量類型:Semaphore和SemaphoreSlim。其中兩者功能基本相同,卻又有所不同,而SemaphoreSlim是更輕量、更快速的信號量實現。下面是兩種簡單比較:
Semaphore: 是基于系統內核實現,屬于內核級別同步,支持跨進程資源同步,因此性能較低,內存占用較大;它可以一次釋放多個信號量,但是沒有提供原生的異步支持;
SemaphoreSlim: 是用戶級別同步,并不依賴系統內核,因此不支持跨進程資源同步,因此性能更高,內存占用更低;它一次只能釋放一個信號量,但是提供了原生異步支持;

03、Semaphore使用示例
通過對信號量原理的詳細了解,而作為對信號量實現類Semaphore,這些原理也同樣適用,因此Semaphore類的構造函數就指定了用于控制線程數量的參數。其構造函數如下:
public Semaphore(int initialCount, int maximumCount);
public Semaphore(int initialCount, int maximumCount, string name);
initialCount: 初始化信號量的計數,表示初始時可以同時訪問資源的線程數量。
maximumCount: 信號量的最大計數,表示允許同時訪問資源的最大線程數。
name: 可選的名稱,用于命名信號量對象(可在多個進程間共享信號量)。
然后可以用WaitOne方法獲取信號量,使用Release方法釋放信號量。
下面我們做一個小例子,創建一個初始化為2個線程的信號量Semaphore,然后啟動5個線程用來訪問信號量,并在獲取信號量之前、之后以及釋放信號量之前都加上日志,用來觀察線程被控制的過程。代碼如下:
public class SemaphoreExample
{
//初始化最多2個線程同時進入, 最大允許3個線程
private static Semaphore semaphore = new Semaphore(2, 3);
//用于不同的線程顯示不同的顏色,方便觀察結果
private static ConsoleColor[] colors = new ConsoleColor[5]
{
ConsoleColor.Red,
ConsoleColor.White,
ConsoleColor.Yellow,
ConsoleColor.Green,
ConsoleColor.Blue
};
public static void Worker(object? i)
{
var id = (int)i;
var color = colors[id];
PrintText.SafeForegroundColor($"線程 {id} 等待進入...", color);
//請求進入信號量(如果資源不可用,則返回)
semaphore.WaitOne();
PrintText.SafeForegroundColor($"線程 {id} 已 [ 進入 ] 同步代碼塊.", color);
//業務處理
Thread.Sleep(2000);
PrintText.SafeForegroundColor($"線程 {id} 已 [ 離開 ] 同步代碼塊.", color);
//釋放信號量(讓其他線程可以進入)
semaphore.Release();
}
}
public static void SemaphoreRun()
{
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(SemaphoreExample.Worker);
t.Start(i);
}
}
我們一起看看執行結果:

可以發現在紅框部分為所有線程初始化完成,同時只有兩個線程獲取到信號量,之后就是又一個信號量釋放成功,則緊跟著一個等待線程會立馬進入,直到所有線程處理完成。
到這里會有一個疑問,我們在初始化信號量Semaphore時,指定了最大允許3個線程可以同時進入,但是上面示例并沒有體現出來,這是為什么呢?
這是因為在信號量的生命周期中,maximumCount參數并不會直接改變信號量的當前可用資源數量,而是限制并發線程數的最大值。因此如果要想看到效果,則需要經過特殊處理才行,比如我們在上面的代碼中釋放信號量時,我們執行兩次釋放操作,但是這樣會導致最后釋放操作報SemaphoreFullException異常,因此要注意獲取和釋放信號量操作要配對,這里僅僅為了演示,執行結果如下:

可以看到maximumCount最大訪問線程數生效了。
04、Semaphore使用注意事項
1.確保每個調用 WaitOne方法 的線程最終都會調用 Release方法。否則,可能會導致死鎖。
2.確保Release方法的調用次數不應超過 WaitOne方法的調用次數,否則會拋出 SemaphoreFullException異常
3.如果單進程程序盡量選擇SemaphoreSlim,因為SemaphoreSlim性能更好。
4.如果需要跨進程同步可以使用帶名稱的構造函數 Semaphore(int initialCount, int maximumCount, string name)。
注:測試方法代碼以及示例源碼都已經上傳至代碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

浙公網安備 33010602011771號