1. 介紹
Livelock 是在多執行緒應用裡的一種情況,當兩個或更多執行緒試圖避免互相死鎖(Deadlock)時,結果卻不斷對彼此的動作做出反應,但沒往前推進任何實際工作。 與 Deadlock 不同,那裡執行緒僵住、什麼都做不了;Livelock 裡程式看起來是「活著」的:執行緒在跑,但沒有人做有用的事。
就像你和同事在窄長走廊裡不斷後退以讓對方先過,結果誰也沒過去。從旁看很滑稽,但在程式裡很糟糕。
Livelock 範例:門口的程式設計師
想像兩個非常有禮貌的程式設計師在窄走廊相遇。他們同時到門口,兩個人都決定退讓:往旁邊退一步讓對方。兩個人又發現情況沒變,再次退讓 — 無限循環。沒有人靜止,大家都在動,但沒有人通過門。
在多執行緒程式中看起來像:執行緒看到資源被佔用,就退一步,重新檢查,再退一步,如此無止盡。
2. Livelock 的實作示例
我們用程式碼實作類似情況。假設有兩個資源(例如兩個銀行帳戶),有兩個執行緒同時試圖互相轉帳,並且都想避免「卡住」。
class Account
{
public int Balance { get; set; }
public object LockObj { get; } = new object();
}
public class Program
{
static void Transfer(Account from, Account to, int amount)
{
while (true)
{
bool lockedFrom = false;
bool lockedTo = false;
try
{
Monitor.TryEnter(from.LockObj, 100, ref lockedFrom);
Monitor.TryEnter(to.LockObj, 100, ref lockedTo);
if (lockedFrom && lockedTo)
{
from.Balance -= amount;
to.Balance += amount;
break; // 轉帳完成!
}
}
finally
{
if (lockedFrom) Monitor.Exit(from.LockObj);
if (lockedTo) Monitor.Exit(to.LockObj);
}
// 沒成功?退後再試 — 很有禮貌!
Thread.Sleep(1);
}
}
// ... 啟動兩個執行緒呼叫 Transfer(A, B, ...); Transfer(B, A, ...);
}
在這個範例中,兩個執行緒會反覆嘗試取得鎖。但如果兩邊一直看到鎖被佔用並選擇退讓,最終就變成「有禮貌地」互相退讓而沒有任何轉帳發生。靠作業系統排程的隨機性來解決這事並不是可靠策略。要加入暫停和 backoff,別把短暫的「空迴圈」放在 Monitor.TryEnter 與 Thread.Sleep 周期裡。
Livelock 與 Deadlock 的比較
| Deadlock | Livelock | |
|---|---|---|
| 執行緒 | 停止等待 | 積極運行但在等待 |
| 資源 | 永遠被鎖住 | 明顯沒有被鎖死 |
| CPU | 幾乎不被佔用 | 可能 100% 飆高 |
| 解決方法 | 超時、鎖順序 | 隨機性、暫停、backoff |
3. Starvation(飢餓):資源沒輪到你的情況
Starvation(字面「飢餓」)是指一個或多個執行緒一直被忽略,無法取得需要的資源,因為其他執行緒總是搶在前面。
與 Deadlock 和 Livelock 不同,這裡沒有人被永遠鎖住或不斷退讓 — 只是某些執行緒總是拿不到「那一塊蛋糕」。
實例說明:現實生活類比
想像食堂有兩條隊:「VIP」(例如員工專用通道)和「一般人」。VIP 隊很短,但總是有人先跑進去。一般人可能排半小時,眼看著 VIP 一次次先被服務。這就是 Starvation!
4. 在 C# 中的 Starvation:實務與除錯
看一個關於 lock 的情境:如果一個執行緒優先級很高,它總能進入臨界區,而另一個執行緒一直「落後」:
private static readonly object _locker = new object();
static void Greedy()
{
while (true)
{
lock (_locker)
{
Console.WriteLine("貪婪執行緒已取得資源...");
Thread.Sleep(10); // 更久地持有鎖
}
Thread.Sleep(1);
}
}
static void Poor()
{
while (true)
{
lock (_locker)
{
Console.WriteLine("可憐的執行緒嘗試進入...");
}
}
}
如果兩個執行緒同時啟動,"Greedy"(「貪婪」)會長時間持有鎖並快速再次拿到它,而 "Poor"(「可憐」)幾乎沒機會進來,實際上就是在飢餓。真實場景中的 starvation 可能沒那麼明顯,例如某個執行緒優先級較低或更常被 OS 搶走時間片。
Starvation 出現在哪裡?
- 低優先級的執行緒容易被餓死。
- 任務隊列設計不當(沒有公平的 FIFO 策略)。
- 在 ReaderWriterLockSlim 的預設設定下:如果寫者很少來但讀者源源不絕,寫者會一直飢餓,因為讀者佔著 Read Lock,不讓出 Write Lock。
5. 為什麼 Starvation 不一定是 bug,但始終是問題
Starvation 與 Deadlock 不同,它不會讓整個程式停擺,但會造成結果不穩定與不公平:資料處理不均、有些任務等到永遠、效能下降。這在伺服器端特別危險,因為所有任務應該有合理的機會被處理。
如何偵測與診斷 Livelock 和 Starvation
- CPU 使用率猛增,但任務「卡住不動」— 可能是 Livelock。
- 執行緒似乎在工作,但某些任務永遠不完成 — 可能是 Starvation。
- 日誌:如果 log 顯示某些執行緒或任務從未獲得資源訪問權限 — 幾乎可以確定是飢餓。
如何避免 Livelock
- 不要只靠像 Monitor.TryEnter 這種「讓步式」non-blocking 方法。若失敗,加入隨機暫停(Thread.Sleep(Random.Next(1, 10)))),避免執行緒時間點完全對齊。
- 避免鞏固短暫的忙等迴圈,否則「退讓—嘗試」的迴圈會變成無限循環。
- 有時改變演算法會有用:引入「長者權」或某種優先規則,讓只有某些執行緒能退讓,其他則不能。
如何避免 Starvation
- 留意執行緒優先級:背景或高優先的工作不應該永遠壓榨其他工作。
- 使用公平的隊列(ConcurrentQueue<T>、channels、task queue)和 FIFO 策略,確保任務按順序被處理。
- 在 ReaderWriterLockSlim 中可以暫停新的讀者以優先服務寫者,讓寫者終於能取得 Write Lock。
- 使用超時與記錄:如果執行緒嘗試拿鎖太久,就輸出警告。
- 縮短持鎖時間並拆分關鍵區塊。
Deadlock、Livelock 與 Starvation 的比較
| 情境 | Deadlock | Livelock | Starvation |
|---|---|---|---|
| 執行緒 | 死止不動 | 跑來跑去但白忙一場 | 有人在工作,有人沒得到機會 |
| 資源存取 | 沒有 | 沒有 | 不均勻,有時沒有 |
| CPU | 不被佔用 | 被大量佔用 | 被佔用,但不是所有執行緒 |
| 是嚴重錯誤嗎? | 是 | 是 | 有時是 |
GO TO FULL VERSION