在作業系統中,核心任務之一是有效地管理和分配有限的資源給多個同時運行的threads或processes。一個良好的系統模型能讓我們理解資源如何在競爭的環境中被請求、使用與釋放。
資源是指任何執行緒在執行過程中可能需要使用的軟硬體元件。它們通常是有限的,這意味著同一時間能被使用的數量是固定的。資源可以大致分為兩類:
第一種,硬體資源(Hardware Resources):
第二種,軟體資源(Software Resources):
每個執行緒在與資源互動時,都必須遵循一個標準的三階段生命週期:
第一階段,請求 (Request)
第二階段,使用 (Use)
第三階段,釋放 (Release)
死結是多執行緒程式中最難以解決的同步問題之一。當多個執行緒彼此佔有資源並互相等待對方所佔有的資源時,就會發生死結,導致所有執行緒都無法繼續執行。
// 建立了兩把鎖 first_mutex 和 second_mutex
pthread_mutex_t first_mutex;
pthread_mutex_t second_mutex;
// 建立兩個執行緒 thread_one 和 thread_two
pthread_mutex_init(&first_mutex, NULL);
pthread_mutex_init(&second_mutex, NULL);
// 執行緒函數:
// 執行緒一
void *do_work_one(void *param) {
pthread_mutex_lock(&first_mutex); // 鎖住第一把鎖
pthread_mutex_lock(&second_mutex); // 嘗試鎖住第二把鎖
// 做一些工作...
pthread_mutex_unlock(&second_mutex); // 釋放第二把鎖
pthread_mutex_unlock(&first_mutex); // 釋放第一把鎖
pthread_exit(0);
}
// 執行緒二
void *do_work_two(void *param) {
pthread_mutex_lock(&second_mutex); // 鎖住第二把鎖
pthread_mutex_lock(&first_mutex); // 嘗試鎖住第一把鎖
// 做一些工作...
pthread_mutex_unlock(&first_mutex); // 釋放第一把鎖
pthread_mutex_unlock(&second_mutex); // 釋放第二把鎖
pthread_exit(0);
}
範例中,有兩把鎖 (first_mutex 和 second_mutex),以及兩個執行緒 (thread_one 和 thread_two)。死結發生在一個特定的、不幸的執行順序下:
結果是:thread_one 在等待 thread_two 釋放資源,而 thread_two 則在等待 thread_one 釋放資源。兩者都無法前進,形成了無限期的互相等待,這就是死結。
程式碼之所以會發生死結,是因為它同時滿足了死結發生的四個必要條件:
死結之所以棘手,是因為它不是一個固定的錯誤,而是一種非確定性(non-deterministic)的行為。這使得在開發和除錯過程中很難發現和重現。
第一,非決定性與排程影響:死結的發生完全取決於執行緒的排程順序和相對執行的時間點。如果 thread_one 運行得非常快,在 thread_two 執行前就完成了所有操作,那麼死結就不會發生。反之,如果兩個執行緒在錯誤的時機點交錯執行,就會卡住。
第二,難以重現:由於排程器行為的不可預測性,同樣的程式碼在不同電腦、不同作業系統版本,甚至在同一台電腦的不同運行時間下,都可能表現出不同的行為。這讓程式設計師難以可靠地重現死結,從而無法進行有效的除錯。
第三,除錯困難:當死結發生時,程式不會崩潰或顯示錯誤訊息。它只是**「卡住不動」**,看起來就像是程式反應變慢了。這很容易誤導開發者,讓他們以為是程式效能問題,而不是同步錯誤。
活結(Livelock)是一種特殊的同步問題,它會導致執行緒無法取得進展,但與死結不同的是,執行緒並沒有被阻塞。它們處於一種忙碌的重試循環中,不斷地改變狀態,卻永遠無法完成工作。
活結最核心的特徵是「積極的失敗」。執行緒們沒有在等待,而是在**主動「讓路」**或「重試」,但由於步調一致,導致它們永遠無法突破困境。你的比喻非常生動地描繪了這個情境:在走廊上相遇的兩人,互相閃避卻不斷碰撞,陷入無限的讓路循環。
// 活結的情境
while (1) {
pthread_mutex_lock(&first_mutex);
if (pthread_mutex_trylock(&second_mutex) != 0) {
// 嘗試鎖第二把鎖失敗,主動釋放第一把鎖並重試
pthread_mutex_unlock(&first_mutex);
continue; // 再來一次
}
// 成功鎖住兩把鎖
// ... 做一些工作 ...
pthread_mutex_unlock(&second_mutex);
pthread_mutex_unlock(&first_mutex);
break;
}
在這個範例中,一個執行緒會嘗試鎖住兩把互斥鎖。如果第二把鎖被佔用,它會主動釋放已持有的第一把鎖,然後從頭開始重試。假設有兩個執行緒,各自運行著這段程式碼,並在相同的時間點啟動:
結果是,兩個執行緒都在不斷地釋放資源、重試、失敗、再釋放,陷入一個不斷變換狀態但沒有任何進展的死循環。它們沒有被阻塞,CPU 佔用率可能很高,但工作卻停滯不前。
為了打破同步碰撞的循環,常見解法是在重試之間插入隨機延遲(Random Delay / Backoff)。
#include <unistd.h> // for usleep
#include <stdlib.h> // for rand
while (1) {
pthread_mutex_lock(&first_mutex);
if (pthread_mutex_trylock(&second_mutex) != 0) {
pthread_mutex_unlock(&first_mutex);
// 加入隨機等待時間(1ms ~ 5ms)
int backoff = (rand() % 5 + 1) * 1000; // 單位:微秒
usleep(backoff);
continue;
}
// 成功鎖定兩把鎖
// ... 執行工作 ...
pthread_mutex_unlock(&second_mutex);
pthread_mutex_unlock(&first_mutex);
break;
}
當兩個執行緒都在忙碌重試時,隨機退避機制會讓它們的重試時間錯開。