前兩天我們已經認識了兩種 Synchronization Mechanism:
Mutex / Semaphore
但是還有另一種 Lock,它的想法完全不同:
Thread B:
鎖好了嗎?
鎖好了嗎?
鎖好了嗎?
鎖好了嗎?
鎖好了嗎?
...它不睡覺。甚至一直佔著 CPU 等。
這就是:
但問題來了:
既然 Busy Waiting 會浪費 CPU,為什麼 Linux Kernel 這類系統底層仍然需要 Spinlock?
答案跟「等待到底有多短」以及「睡眠本身也是有成本的」有很大的關係。
Spin 就是:Thread / CPU 不斷重複檢查 Lock 是否已經可以取得。
假設:
Thread A
│
│ acquire
▼
Spinlock
│
▼
Critical Section
這時 Thread B 也想取得同一把 Spinlock:
Thread B
│
│ acquire
▼
發現 Spinlock 已被 A 持有 B 不會立刻:Running ->Sleeping
而是:Check Lock
↓
Locked
↓
Check Again
↓
Locked
↓
Check Again
↓
Locked
↓
...
這就是:Busy Waiting
因為 B 仍然在 CPU 上執行 Instructions。
讓一條 Thread 睡著,再把它叫醒,也不是免費的。
假設 Thread B 拿不到 Mutex,系統可能需要讓 B 從 Running 轉成等待狀態:Running -> Blocked / Waiting
接著 Scheduler 可以安排其他工作:CPU
Thread B
│
│ Block
▼
Thread C
等 Lock 可以取得時,又需要:Thread B
Waiting
↓
Ready
↓
Scheduler 選到它
↓
Running
整個過程可能涉及:
也就是:
Sleep 並不是按下一個免費的暫停鍵。
為了避免浪費幾個 CPU cycles,反而付出了更大的 Block / Wakeup / Scheduling 成本。
所以另一種想法就是,B:我先不要睡。
反正 A 應該馬上就好了,我在這裡 Spin 一下。
那 Spin 反而可能比較划算。
假設只有:One CPU Core
Thread A:取得 Spinlock
↓
進入 Critical Section
接著某種情況下 CPU 去執行 Thread B,B 發現:
Spinlock 被 A 持有,於是開始:
Spin
Spin
Spin
Spin
Spin
假設現在:
Core 1 → Thread A
Core 2 → Thread B
A 持有 Spinlock,同一時間:
Core 2:
Thread B
│
▼
Spin
Spin
Spin
│
│ A unlock
▼
取得 Lock
就合理很多。因為 B 在 Core 2 上 Spin 的同時:
持有 Lock 的 A 仍然可以在 Core 1 上繼續執行,最後釋放 Lock。
(1)Spinlock 最核心的行為:
想取得 Lock
│
▼
成功了嗎?
│ │
Yes No
│ │
▼ ▼
進入 Spin
Critical │
Section │
└────► 再試一次
(2)它和 Mutex 最大的直觀差異在於:
Mutex
拿不到 → 可以 Block
Spinlock
拿不到 → Busy Waiting
Spinlock 的代價非常明顯:等待期間仍然消耗 CPU
(3)它存在的原因:
Critical Section 很短
+
Lock 預期很快釋放
+
Sleep / Wakeup 本身有成本
+
某些 Kernel Context 不能 Sleep
↓
Spin 可能比 Block 更合理
從 Day 8 到現在,其實已經形成一條很完整的 Synchronization 路線:
Race Condition
↓
Atomicity
↓
Mutex
↓
Semaphore
↓
Spinlock
但到目前為止,我們一直假設:
只要大家乖乖使用 Lock,事情就會變安全,但是,Lock 本身也能把程式搞死,假設:
A 在等 B。
B 也在等 A。
兩個都沒有辦法繼續,就是卡在那裡了。
下一篇: