但現在換一個問題。
假設系統裡不是只有「一份」資源,而是:
Database Connection Pool
Connection 1
Connection 2
Connection 3
我們希望:
三個 Thread 可以同時工作。
Unlocked → 可以進一個
Locked → 其他全部等
就沒有直接表達:
「我有 3 份資源,所以最多允許 3 個使用者同時進來。」的能力。
這時候就需要另一個很經典的 Synchronization Primitive:
Semaphore
Semaphore 最核心的概念,可以先理解成:
由同步機制管理的一個計數狀態,Thread 必須透過特定的原子操作取得或歸還許可。
假設現在有三份相同類型的資源:
Resource A
Resource B
Resource C
並建立Semaphore = 3
這個 3 可以理解成:目前有 3 個 permit(許可)可以被取得。
Thread A 想使用資源:
Semaphore = 3
│
Thread A 取得一個 permit
▼
Semaphore = 2
Thread B:
Semaphore = 2
│
Thread B 取得一個
▼
Semaphore = 1
Thread C:
Semaphore = 1
│
Thread C 取得一個
▼
Semaphore = 0
Thread D
│
│ 想取得 permit
▼
Semaphore = 0
│
▼
目前沒有可取得的 permit
│
▼
等待
等 Thread B 做完工作,把 permit 歸還:
Thread B 完成
│
│ 歸還
▼
有一個 permit 再次可用
D 才有機會繼續。
所以 Semaphore 和普通整數:
int count = 3;
完全不是同一回事。
Thread A:
讀到 count = 1
→ 可以
Thread B 同時:
讀到 count = 1
→ 我也可以
最後兩邊都以為自己成功取得最後一個資源。
所以 Semaphore 不能只是「一個會加減的變數」。
檢查狀態、取得 permit,以及必要時進入等待,都必須由同步機制正確地協調。
這也是為什麼 Semaphore 是 Synchronization Primitive,而不是我們自己宣告一個 int 就完成了。
Semaphore 最常看到兩個操作。
不同 API 名稱會不太一樣,例如 POSIX semaphore 常看到:
sem_wait(&sem);
sem_post(&sem);
在教科書裡也常看到:
wait()
signal()
(1)wait():我要取得一個 permit
Thread A:sem_wait(&sem);
如果目前有可取得的 permit:
3
↓
2
A 成功繼續執行。
Thread B:
2
↓
1
Thread C:
1
↓
0
但是 D:此時已經:Semaphore = 0 所以 D 不能再直接取得 permit。
等有 permit 可以取得時,它才有機會被喚醒並繼續。
(2)post():我歸還/釋出一個 permit
假設 Thread A 工作完成:這代表它釋出一個 permit。
如果已經有等待者:
Thread D → Waiting
Thread E → Waiting
post() 則可以使其中的等待者有機會繼續。
Semaphore 的狀態改變後,等待者可以依實作與排程規則被喚醒並重新參與執行,真正什麼時候在 CPU 上 Running,仍然涉及 Scheduler。
(3)為什麼 wait() 一定要具有 Atomicity?
「檢查還有沒有 permit」和「取得 permit」不能是兩個可以被其他競爭者隨便插入的操作。
也就是:Check + Decrement / Acquire
必須被同步機制安全地完成。
Counting Semaphore
也就是 Semaphore 的 permit 數量可以大於 1。
Semaphore 管的是「許可」,不一定就是實體資源本身
假設:Semaphore = 3
不代表 Semaphore 裡面真的裝著:
Database Connection 1
Database Connection 2
Database Connection 3
Semaphore 本身主要是在控制:最多允許 3 個 Thread 同時取得/使用這類有限資源,現在允許多少個執行者通過這個同步點。它比較像資源入口的「名額控制」,它只負責:現在還能不能再允許一個使用者進來。
Semaphore 初始值設成:Semaphore = 1
Thread A:wait() → 1 → 0
A 可以進入。
Thread B:wait() → 目前沒有 permit → 等待
A:post() 之後 B 才有機會繼續。這種只有:0 或 1
兩種 permit 狀態的 Semaphore,常稱為:Binary Semaphore
(1)ㄟ可是這不就是 Mutex 嗎?
Mutex
→ 同一時間一個持有者
Binary Semaphore
→ 同一時間最多一個 permit
Mutex 強調 Ownership,最重要的一個概念是:Thread A,lock(mutex)
成功後:Mutex Owner = Thread A
而取得 Mutex 的 Thread,負責釋放這把 Mutex,也就是:
Thread A lock
↓
Critical Section
↓
unlock
Semaphore 則是 Permit
wait()
→ 消耗/取得一個 permit
post()
→ 增加/釋出一個 permit
它通常沒有 Mutex 那種「這把鎖屬於某個 Thread」的 ownership 語意。
它能表示:
某件事情發生了幾次/是否已經允許另一個執行流程繼續。
B 一定要等 A 把資料準備好,才能開始,如果沒有 Synchronization:我們不能靠:sleep(1);然後祈禱 A 一秒內一定完成。可以建立:
Semaphore = 0
為什麼是 0?因為一開始:「資料完成」這個 permit 還不存在
Thread B:wait(sem)
因為值是 0 ; B → Waiting
Thread A:
讀取資料
↓
處理完成
↓
post(sem)
現在 B 才能繼續。
在建立一種: Synchronization Relationship。
如果 Thread 成功 wait(),卻忘了:sem_post(&sem);會發生什麼?
假設:Semaphore = 1
Thread A:wait()
↓
1 → 0
然後 A 做完直接離開:忘記 post()
此時,Semaphore = 0,Thread B / C:
wait()
↓
等待
如果之後再也沒有人 post(),大家可能永遠沒有 permit 可以取得。
wait()
→ 嘗試取得一個 permit
→ 沒有時可能 Block
post()
→ 釋出/增加一個 permit
→ 可能讓等待者有機會繼續
當 Mutex 拿不到時,我們說 Thread 可以:Blocked / Sleeping
可是有另一種 Lock 卻選擇:不睡。我就在 CPU 上一直等。
下一篇: