iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
Software Development

用作業系統讀懂另一半的OS系列 第 15

【2025鐵人賽】用作業系統讀懂另一半的OS:Synchronization Tools 02

  • 分享至 

  • xImage
  •  

今天研究所上有發生些事...總結來說,研究所真的是一個很尷尬的時期,因為沒有薪資(畢竟不是勞工),但實際生活卻不像學生...
有時會面臨一些複雜的狀況。當一個人盡力擔負起責任,試圖解決問題時,最終卻發現自己也成了被指責的對象。
願意付出,結果卻事與願違的感受,或許只有親身經歷的人才能體會。

Peterson's Solution(彼得森解法)

Peterson's Solution是一種只用軟體就能實現兩個 process 的互斥臨界區的演算法。它是 OS 同步中的經典演算法,儘管在現代硬體上不保證正確,但它非常適合用來:

  • 示範如何透過程式邏輯實現臨界區控制
  • 解釋三個同步條件:Mutual Exclusion、Progress、Bounded Waiting

Peterson's Solution的適用條件:僅限兩個 process(P0 與 P1),Process 之間會輪流進入 critical section。

// 共享兩個變數
int turn;          // 輪到誰(0 或 1)
bool flag[2];      // flag[i] 表示 Pi 想要進入臨界區
// 演算法流程(Process Pi 的程式碼)
while (true) {
    flag[i] = true;        // 宣告自己要進入 CS
    turn = j;              // 禮讓對方先選
    while (flag[j] && turn == j){} // 忍耐等待

    // 🔐 Critical Section
    ...

    flag[i] = false;       // 我執行完畢,退出 CS

    // 🧘 Remainder Section
}
// 模擬狀況
state 初始:flag[0] = flag[1] = false
假設P0想進入:
flag[0] = true
turn = 1

假設P1想進入:
flag[1] = true
turn = 0

while 條件:
    P0: flag[1] && turn==1 → ❌ 不滿足(turn = 0)
    P1: flag[0] && turn==0 → ✅ 卡住

➡ P0 可以進入

以三大同步條件來回顧Peterson’ s Solution(彼得森解法):

  • ✅Mutual Exclusion(互斥):同時最多只會有一個人成功通過 while 條件
  • ✅Progress(進度):沒有 process 在 critical section 時,不會無限等待
  • ✅Bounded Waiting(有限等待):最多只會等一次對方執行完臨界區,不會無限 starve

Hardware Support for Synchronization(硬體支援的同步工具)

在多核心或多執行緒環境中,若多個行程或執行緒同時存取共享變數,就可能會產生race condition,導致資料錯誤或程式邏輯異常。雖然我們可以使用Peterson’s Solution等軟體層級的協議來實現互斥(mutual exclusion),這些方法在理論上可以滿足互斥、前進性、有限等待三大條件,但它們在現代多核心系統中可能會失效,原因包括:

  • 編譯器最佳化 可能重排序讀寫指令
  • CPU 指令重排(instruction reordering)
  • 快取一致性問題:不同核心可能對共享記憶體有不同的本地快取版本,導致檢查的狀態不一致
  • 缺乏真正的原子操作(atomic operation)支援

也因此,在現代系統中,硬體層級的同步原語是實現安全互斥的必要條件。

Memory Barriers(記憶體障壁)

在現代多核心系統中,為了效能:

  • 編譯器會重排讀寫順序(只要單一執行緒語意不變)
  • CPU 為了提昇效率也可能「亂序執行」
  • 各核心有獨立快取(Cache) → 資料更新時間點可能不一致

這些都可能導致 多執行緒同步錯誤(尤其 race condition 難以重現)。而Memory Barrier 是一種特殊的 CPU 指令或編譯器指令,用來防止指令重排序(reordering),確保指令的執行順序與你寫的程式順序相同。

// Thread 1
while (!flag)
    memory_barrier();  // 確保讀 flag 之前不會偷跑去讀 x
print x;
// Thread 2
x = 100;
memory_barrier();     // 確保 x = 100 寫入後,flag 才會設為 true
flag = true;

記憶體模型(Memory Model)

不同的硬體架構定義了不同的記憶體模型(memory consistency model),用來規範記憶體操作的順序性與可見性。

  • Strongly Ordered:所有記憶體操作都依程式順序執行,一個處理器對記憶體的寫入會「立即」對其他處理器可見。較常見於 x86 架構。
  • Weakly Ordered:CPU 允許對指令進行重新排序,寫入可能延遲被其他核心看到。常見於 ARM、PowerPC、RISC-V 等高效能處理器。

硬體原子指令(Hardware Instructions)

為什麼需要硬體原子操作?

軟體實作(如 Peterson’s Solution)在多核心系統中可能失效。而硬體提供了不可中斷的原子操作,確保共享變數存取過程不被其他 process/thread 插入

測試並設定(Test-and-Set)

// 定義 test-and-set
bool test_and_set(bool* target) {
    bool rv = *target;
    *target = true;
    return rv;
}
// 使用 test-and-set:(這段操作是「原子」進行,兩個核心同時執行也不會競爭失敗)
bool lock = false;

while (test_and_set(&lock)) ;  // busy-wait

// critical section
lock = false;  // 解鎖

以三大同步條件來回顧test-and-set:

  • ✅Mutual Exclusion(互斥):test_and_set() 為原子操作,一次只會有一個執行緒成功取得鎖。
  • ✅Progress(進度):若沒有執行緒在臨界區,則一定會有一個執行緒成功取得鎖
  • ❌Bounded Waiting(有限等待):沒有保證每個執行緒都會在有限次內取得鎖,可能出現飢餓(starvation),因為某些執行緒可能一直在競爭中失敗。→ 飢餓(starvation)

為何會出現 starvation(飢餓)? OS沒有FCFS的排隊機制,多個執行緒同時 busy-wait

Compare-and-Swap(比較並交換,CAS)

// 定義 CAS:
int compare_and_swap(int* value, int expected, int new_value) {
    int temp = *value;
    if (*value == expected)
        *value = new_value;
    return temp;
}
// 使用 CAS:(這段操作是「原子」進行,兩個核心同時執行也不會競爭失敗)
int lock = 0;

while (compare_and_swap(&lock, 0, 1) != 0) ;  // busy-wait

// critical section
lock = 0;  // 解鎖

以三大同步條件來回顧CAS:

  • ✅Mutual Exclusion(互斥):compare_and_swap() 為原子操作,保證只有一個執行緒能將 lock 設為 1。
  • ✅Progress(進度):沒有其他執行緒在臨界區時,必有一個執行緒會成功設置 lock。
  • ❌Bounded Waiting(有限等待):同樣沒有保證公平性,可能某個執行緒永遠搶不贏(饑餓),這稱為 lock starvation。

補充:為何test-and-set跟CAS都無法滿足bounded waiting:
兩者都採用 busy-wait 自旋,而且沒有先來先服務(FIFO)或排隊機制。執行緒的搶鎖成功機率可能受排程與 CPU 核心切換影響,導致某些執行緒長時間得不到進入機會。如果要滿足有限等待,需額外設計waiting 陣列實現有限等待,保證每個 process 最多等待 n-1 次就可以進入 critical section。

互斥鎖 (Mutex Locks)

Mutex Locks是一種作業系統提供的「高階、易用、抽象」的同步工具,用於保護共享資源,確保在任何給定時間內,只有一個執行緒(或進程)可以訪問臨界區(Critical Section)。

核心概念與運作原理

互斥鎖的設計目的在於解決底層硬體原子操作(如 test-and-setCAS)不直觀、不易使用的問題。它將底層操作封裝起來,讓程式設計師可以更簡單地使用 acquire()release() 兩個操作來管理共享資源。

  • acquire(): 嘗試獲取鎖。如果鎖是可用的,就成功取得鎖並進入臨界區;如果鎖已被其他執行緒佔用,則呼叫此函式的執行緒會進入睡眠狀態(block),讓出 CPU,等待鎖被釋放。
  • release(): 釋放鎖。當執行緒完成對共享資源的操作後,呼叫此函式來釋放鎖,並喚醒一個正在等待的執行緒,使其可以繼續執行。

這個過程就像你比喻的:「先去喝咖啡,等有人用完再叫我回來。」這種讓出 CPU 的等待方式,是 Mutex 的核心特徵,有效避免了 CPU 資源的浪費。

// 典型的 Mutex 使用流程
while (true) {
    acquire_lock();        // 嘗試獲取鎖,若失敗則進入睡眠
    // critical section    // 只有一個執行緒能執行的區域
    release_lock();        // 釋放鎖,讓其他等待的執行緒有機會
    // remainder section   // 非共享資源操作
}

Contention(爭用)

Contention指的是當多個執行緒同時嘗試獲取同一把鎖的情況。這是理解 Mutex 與 Spinlock 差異的關鍵。

  • 無爭用鎖(uncontended lock): 沒有其他執行緒競爭,執行緒可以立即取得鎖,無需等待。
  • 有爭用鎖(contended lock): 有一個或多個執行緒正在競爭同一把鎖。此時,除了取得鎖的執行緒外,其他所有競爭者都必須等待

自旋鎖 (Spinlock)

Spinlock是一種特殊的鎖機制。當執行緒嘗試獲取鎖但發現它被佔用時,它不會進入睡眠狀態,而是會進入一個忙等(busy-wait)迴圈,不斷重複檢查鎖的狀態,直到鎖可用為止。

一直站在門口盯著影印機,問『好了沒?好了沒?

  • 優點: 如果臨界區非常短,自旋鎖可以避免上下文切換(context switch)的開銷。因為上下文切換需要時間,如果鎖很快就會被釋放,自旋等待反而比切換到睡眠狀態再被喚醒更有效率。
  • 缺點: 如果臨界區很長,自旋鎖會持續佔用 CPU 資源,造成嚴重的效能浪費,特別是在單核心系統上。

Mutex vs. Spinlock 比較

比較項目 自旋鎖 (Spinlock) 互斥鎖 (Mutex)
等待方式 忙等(busy-wait),持續消耗 CPU 週期。 阻塞(block),進入睡眠狀態並讓出 CPU。
效率特性 適合臨界區極短的場景,可以避免上下文切換的開銷。 適合臨界區較長的場景,可以避免浪費 CPU 資源。
實作方式 通常由底層硬體原子操作(如 CAS 或 test-and-set)加上一個忙等迴圈實現。 透過作業系統的系統呼叫(System Call)來實現,例如 pthread_mutex_lock()
資源使用 高,因其持續佔用 CPU。 低,因其讓出 CPU 給其他執行緒。
適用場景 多核心系統,且鎖被佔用的時間極短。 單核心系統或鎖被佔用時間較長的任何場景。

回顧 Mutex 的同步條件

我們可以用三大同步條件來評估 Mutex 的特性:

  1. ✅互斥(Mutual Exclusion): Mutex 的設計核心就是互斥,確保任何時間點只有一個執行緒可以進入臨界區。這是它最基本的保證。
  2. ✅進度(Progress): 如果沒有任何執行緒在臨界區內,那麼在等待鎖的執行緒中,有一個終將會被選中並取得鎖,從而使系統能夠繼續前進。這通常由作業系統的排程器(Scheduler)來決定。
  3. ❌有限等待(Bounded Waiting): 一個標準的 Mutex 不保證有限等待。這意味著,如果有多個執行緒在競爭同一把鎖,排程器可能重複選擇某些執行緒而忽略其他,導致某些執行緒長期無法取得鎖(Starvation,飢餓)。為了避免這種情況,需要額外實作公平的排隊機制。

上一篇
【2025鐵人賽】用作業系統讀懂另一半的OS:Synchronization Tools 01
下一篇
【2025鐵人賽】用作業系統讀懂另一半的OS:Synchronization Tools 03
系列文
用作業系統讀懂另一半的OS30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言