CodeGym /課程 /C# SELF /其他問題:Livelock 和 Starvation

其他問題:Livelock 和 Starvation

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

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 不被佔用 被大量佔用 被佔用,但不是所有執行緒
是嚴重錯誤嗎? 是 是 有時是
2
任務
C# SELF, 等級 57, 課堂 3
上鎖
飢餓現象示範
飢餓現象示範
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION