# 作業系統 CH6 Process Synchronization
###### tags: `作業系統 Operating System`
當共享的資料同時被不同 Process / threads 存取時,因為執行順序的不確定性,很容易發生 data inconsistency 的狀況,所以需要額外的機制來確保程式的正確性,也就是 Synchronization
以下為常見的 Consumer & Producer Problem
![](https://i.imgur.com/pUmSk1u.png)
除了 Buffer 之外, Counter 也是 share variable,要注意的是 `counter++` 及 `counter--` 在 instruction level 其實有三道指令,以 `counter++` 為例,要先將 counter 從記憶體中移到 register,加上 1 之後,再存回記憶體。由於 `context switch`, `Preemptive scheduling` 等因素, 這些指令可能不是一次執行完成,導致執行結果不符合預期
```
move ax, counter
add ax, 1
move counter, ax
```
![](https://i.imgur.com/cnUIh7W.png)
## Race Condition
當多個 Processes 同時存取及操作 shared data 時,最終的值取決於最後一個完成執行的 Process,這個現象稱為 Race Condition
- 在 single-processor machine 中,我們可以 disable interrupt 或著使用 Non-preemptive scheduling 來達成同步,但在 User Program 中不可能使用,會影響整個系統的運作
- 通常將可能產生 Race Condition 的區域稱作 `Critical Section`
## Critical-Section Problem
- 目的:建立 Processes 之間合作的 protocol
- Problem description
- N 個 Processes 競爭使用 shared data
- 每一個 Process 存取 shared data 的 code segment 我們稱為 critical section
- 若我們確保當一個 Process 執行 critical section 時,其他 Processes 不得執行 critical section,稱之為 `mutually exclusive`,是比較暴力的解法
- 一般常見 critical section 的架構如下
![](https://i.imgur.com/yP89tpT.png)
## Critical-Section Requirements
1. Mutual Exclusion:
當一個 Process 執行 critical section 時,其他 Processes 不得執行 critical section
2. Progress:
若沒有 Process 在執行 critical section,且有某些 Processes 希望進入它的 critical section,則 Process 不得在未定義的情況下被推遲,一定要進的去
3. Bounded Waiting:
Process 等待進入 critical section 的期限必須受到限制,不可一直排隊等待
## Critical-Section Solution & Synchronization
### Algorithm for Two Processes
我們先以兩個 Processes 的簡單例子做講解
- 只有兩個 Process P0 & P1
- 共享變數
- int turn
- turn = i --> Pi 可進入 critical section
![](https://i.imgur.com/HoO5RsI.png)
以上的程式碼其實並不完美,這個程式要能順利運作的前提是,兩個 Process 必須輪流執行,但 while loop,並不保證執行的順序,很有可能 Process 0 執行完一次,又想進入 critical section,但 Process 1 還沒執行到 turn = 0 這一行,P0 就會卡在 while loop ,不符合 progress
### Peterson's Solution for Two Processes
為了解決上述 Progress 的問題,我們引入另一個變數 `flag` ,用來表示 Process 是否想要進入 critical section 中,當 `flag[i] = True` 的時候,代表 Pi 已經準備好要進入 critical section,`turn = j` 代表把 key 交給別人。所以 while 條件式 `flag[j] && turn==j` 的意義為檢查其他 Process 是否可進入 critical section,確認沒有其他 Process 要進入 critical section 後,自己再進入 critical section。
要注意如果 `turn = j` 不是把 token 交給別人而是搶過來用,當某個 Process 先進入 critical section,輪到另一個 Process 時也可以 break while 迴圈,違反了 mutual exclusion
![](https://i.imgur.com/V0HzLtR.png)
簡單的證明 Peterson's Solution
- Mutual exclusion : turn 不可能同時為 0 或 1,因此不可能同時進入 critical section
- Progress :
- 若 P1 尚未準備好 --> flag[1] = False --> P0 可以進入
- 若兩者都準備好 --> flag[0] = flag[1] = True
- 若 turn = 0 則 P0 進入,否則 P1 進入
- 以上兩種狀況,Process 都可以順利進入 critical section
![](https://i.imgur.com/QnrZjy8.png)
- Bounded waiting :
- 若 P1 離開 critical section --> flag[1] = False --> P0 可以進入
- 若 P1 離開 critical section 後,繼續執行 while loop --> flag[1] = True --> turn = 0 (覆寫 P0 原先的 turn = 1) --> P0 可以進入
- 以上兩種狀況,P1 進入後接著進去的一定是 P0,符合 Bounded Waiting 的要求
![](https://i.imgur.com/NIBUhSv.png)
### Producer/Consumer Problem
情況一:我們將所有程式碼都放入 critical section,如果 consumer 先 call,因為 buffer 是空的,所以 consumer 會卡在 while 迴圈 ; 這時再 call producer ,因為 consumer 已經在 critical section 裡頭, producer 無法進入導致 ==deadlock==
![](https://i.imgur.com/B4UtgIP.png)
情況二:我們只將可能產生 race condition 的程式碼放進 critical section,雖然執行結果正確,但如果某一個 Process 進入 critical section 其他 Process 進不去,可能導致無謂的 context switch 降低效率
![](https://i.imgur.com/w1SvTUu.png)
## Bakery Algorithm (n processes)
- 在進入 critical section 之前,每一個 Process 會抽號碼牌
- 號碼牌數字最小的先進入 critical section (注意可能有相同的號碼牌,如下方程式碼以 `max` 實作,而 `max` 指令其實有好幾行)
- 號碼牌數字的產生一定是 non-decreasing order; i.e. 1,2,3,3,4,5,5,5...
- 若兩個 Process Pi & Pj 有相同的號碼牌,則 PID 小的先進入
- `choosing[i]` 為是否正在抽號碼牌的 flag,在與其他 Process j 比較之前,我們必須等待 Process j 抽完號碼牌。考慮一個狀況, Process j 比 i 晚抽完號碼牌,但號碼與 i 一樣大,又 j 的 PID 比 i 小,所以 Process j 應該要先執行 ; 如果在 j 還沒抽號碼牌之前就進行比較,此時 num[j] = 0,程式會以為 j 不要執行,反而先執行 i,當 j 抽完號碼牌之後發現數字與 i 相同,因此也可以進入 critical section,但這時 i 已經在裡頭,導致 deadlock。
![](https://i.imgur.com/LDKzFti.png)
## Pthread Lock/Mutex Routines
![](https://i.imgur.com/NsK9x4l.png)
## Condition Variables (CV)
- Condition Variables 代表一個條件,符合該條件時可以觸發 thread 執行某些動作
- 三種 CV 的操作
- wait() : 直到其他 thread 呼叫 signal()/broadcast(),thread 處於等待狀態
- signal() : 喚醒一個等待中的 thread
- broadcast() : 喚醒所有等待中的 thread
- 在 Pthread 中,CV type 為 `pthread_cond_t`
- 使用 `pthread_cond_init()` 來初始化
- `pthread_cond_wait (&theCV, &somelock)`
- `pthread_cond_signal (&theCV)`
- `pthread_cond_broadcast (&theCV)`
![](https://i.imgur.com/CbILTa2.png)
上圖執行順序如下:
1. action() 進入 CS,呼叫 `thread_cond_wait` 並==釋放 lock==
2. counter() 取得 lock 進入 CS
3. counter() 呼叫 `singal()` ,action() 被喚醒,注意這時候 action() 是沒有 lock 的
4. counter() 釋放 lock
5. action() 取得 lock 並執行函式
### ThreadPool Implementation
一次創建一定數量的 threads,讓系統不用一直增刪 threads,並且讓系統可以有效控制 threads 的數量,避免過多的 context switch 反而降低效率,達到效能最佳化。
創建 threads 後,為了讓 threads 執行不同的 function,會建立一個 `threadpool_task_t` 的結構體,存放函式及對應參數的指標,因此 threads 在經過 while loop 被喚醒之後,執行目前看到 `threadpool_task_t` 的函式,等於處理了一個 request,處理完成後進入 wait 狀態,等待下次被喚醒。而 threads 要如何知道被喚醒,其中一個方式是透過 Condition Variable 判斷
以下程式碼為一個簡單的範例:
1. 創建 threads pool 以及 task 的 queue
2. thread 執行 while loop 處於 wait 狀態
3. 若 thread 被喚醒則離開 while loop,並執行 queue 中 pool->head 的函式
4. 執行後 pool->head 加 1,再判斷該值是否等於 `queue_size`,若相等表示已經執行到最後一個函式,將 `pool->head` 設為 0
5. 離開 critical section 執行任務
![](https://i.imgur.com/0XU4Osy.png) ![](https://i.imgur.com/zrjQc75.png)
![](https://i.imgur.com/gq6uGxa.png)
![](https://i.imgur.com/HoiEDvQ.png)
![](https://i.imgur.com/DAE6bDy.png)
## Hardware Support
Critical Section Problem 的發生是因為 shared variable 的修正可能被打斷,若某些指令我們可以一行完成,就可以避免 race condition 的問題。前面都是在說軟體上的支援,若硬體可以將指令變成 `atomic instructions` ,就沒有同步的問題
- atomic : 無法中斷執行的最小單元
- 例如 : `TestAndSet(var)`, `Swap(a,b)`
### Atomic Test And Set()
注意以下程式碼不符合 Bounded waiting
![](https://i.imgur.com/TqFexb0.png)
### Atomic Swap()
先 call Swap 的取得進入 critical section 的權力,執行完成後將 token 還給 lock。注意以下程式碼不符合 Bounded waiting
![](https://i.imgur.com/kLAhE67.png)
## Semaphores
- `Samaphores` 是一個通用的同步處理工具,其紀錄某個資源有多少 units 可以去使用
- 若 # of record = 1 --> binary semaphore, 但其實用 mutex lock 就好
- 若 # of record > 1 --> counting semaphore
- 利用兩個 atomic operations `wait` & `signal` 來存取 (注意與 critical section 的 wait & signal 不同)
- 簡單的 Spinlock 以 semaphore 實作
![](https://i.imgur.com/borGoh9.png)
- Semaphore 為 POSIX 標準庫之一,不屬於 Pthread,所以使用上不限於 thread ,也可以用於 Process
### Non-busy waiting Implementation
Busy waiting (SpinLock) 因為 while loop,執行效率沒有被最佳化,所以相對 busy waiting 就有 non-busy waiting 的實作方式,但 system call 要考慮 context switch、re-scheduing 的影響,成本比 while 來得高。一般來說,等待時間很短的,我們會用 busy waiting,反之則使用 non-busy waiting
- Semaphore 為一個有 `queue` 的結構體,包含 Semaphore 的值以及有哪些 Processes 等待被執行
![](https://i.imgur.com/At2yvF0.png)
- wait() & signal()
- 使用 system calls: sleep() & wakeup()
- 一樣必須為 `atomic` 的操作
- 必須先進行 `value--(++)` 操作,避免進入 if 區域後被 sleep 無法執行
![](https://i.imgur.com/ih8QRMQ.png)
- 由於 `value--(++)` 還有 queue 的 insert, delete 等,必須確保 `wait()` & `signal()` 為 atomic 操作
- single-processor : 透過 disable interrupts
- multi-processor :
- HW Support (e.g. Test-And-Set, Swap)
- SW Support (Peterson's solution, Bakery algorithm)
- Semaphore with Critical Section
- 將 `value--(++)` 以及 queue 的操作包進 critical section
- 因為 `sleep()` 及 `wakeup()` 為 system calls,無須擔心同步的問題, OS 會妥善處理,所以不用包進 critical section 中(也不該包進去)
![](https://i.imgur.com/hywaCFt.png)
### Cooperation Synchronization
- 有些情況不同 Process 間的執行順序很重要,考慮兩個 thread P1 & P2 分別執行 S1 & S2,S2 必須在 S1 完成後才可執行
- 實作方式 : 利用 shared variable `semaphore sync` ,以下程式碼確保 S1 一定先執行完成
![](https://i.imgur.com/dyVgwyn.png)
- 更複雜的情況概念相同,由上述兩個 Processes 的例子做延伸
![](https://i.imgur.com/kdhcPhU.png)
## Classical Synchronization Problems
常見的問題如下:
1. Bounded-Buffer (Producer-Consumer) Problem
2. Reader-Writers Problem
3. Dining-Philosopher Problem
### Reader-Writers Problem
- 標準在處理 I/O 會遇到的問題
- A set of shared data objects
- A group of processes
- reader processes (read shared objects)
- writer processes (update shared objects)
- writer process 對 shared object 有獨佔權
- 不同的種類
- first RW problem : 只要確定 writer 有 access 的獨佔權即可,不關心 reader 要等多久,或要做什麼事情。reader 取得 token 時,可以傳給其他 reader,可能造成 writer 一直被其他 reader 插隊,永遠拿不到 lock
- second RW problem : 不只要確定 writer 的 access 權限,另外當 writer ready 且 object 被釋放時,writer 的優先性必須高於 reader ,不可有新的 reader 進來插隊
#### First Reader-Writer Algorithm
- writer 為 binary semaphore
- Reader 共享一個 block
- `readcount` 計算有幾個人要讀
- 當 `readcount > 1` 時,代表已經有 reader 拿了 lock,所以這個 reader 也可以讀
- 當 `readcount = 1` 時,因為是第一個 reader,可能要等 writer 結束才可讀取
- 當 `readcount= 0` 代表最後一個讀的,讀完後要 signal writer
- `readcount` 為 shared variable,必須受到保護
- Writer可能會有 ==starvation problem== ,因為只要有 reader 在讀,其他 reader 都可以進入 critical section,造成 writer 一直在等待狀態
```cpp
// mutual exclusion for write
semaphore wrt=1
// mutual exclusion for readcount
semaphore mutex=1
int readcount=0;
// writer
Writer(){
while(TRUE){
wait(wrt);
// Writer Code
signal(wrt);
}
}
// reader
Reader(){
while(TRUE){
wait(mutex);
readcount++;
if(readcount==1)
wait(wrt);
signal(mutex);
// Reader Code
wait(mutex);
readcount--;
if(readcount==0)
signal(wrt);
signal(mutex);
}
}
```
### Dining-Philosopher Problem
- 5 個人坐在 5 張椅子上,且有 5 支筷子
- 每個人只會思考或是吃飯
- 思考時與剩下 4 個人沒有互動
- 每個人要用餐需要兩支筷子
- 一個人 1 次只能拿 1 支筷子
- 吃完飯後,將兩支筷子都放下
- 若每個人都拿其中一邊的筷子,就 deadlock 了
- starvation problem
![](https://i.imgur.com/6PAv5Sr.png)
## Monitor
雖然 semaphores 提供非常便利及有效的同步機制,但正確性主要是依賴 programmer
- 所有存取 shared data object 的 processes 都需確保 `wait()` & `signal()` 的執行順序及位置正確
- 但是人總有可能犯錯
- 可以將 Monitor 想成一個特殊 class (high level language construct),每一個 Monitor 都有可能發生 race condition 的 local variable (對應下圖的 shared data) 需要保護,而要操作這些 var 必須透過 Monitor 的 method。
- Monitor 與 OO 最重要的差異為,Monitor 一次只有一個 thread 可以執行其 method。(但可以很多個 threads 同時 call 這個 method,只是會處於 inactive 狀態)
- monitor 利用 queue 做排程,確保一次只有一個 process 是 active 的,保護 shared data
![](https://i.imgur.com/QYrkkLB.png)
### Monitor Condition Variables
- 為了允許 process 等待 monitor,需要宣告 condition variable,例如 `condition x, y;`
- condition variable 只能與 `wait()` 及 `signal()` 一起使用
- x.wait()
代表調用這個 operation 的 process 被閒置,直到其他 process 完成為止
- x.signal()
啟動一個等待中的 process,若沒有 process 處於等待狀態,則 `signal()` 操作沒有任何影響。與其相反,semaphore 的 `signal()` 一定會改變 semaphore 的狀態 (`wait` 把 counter 減 1; `signal` 把 counter 加 1)
- 在 monitor 當中,condition variable 不是 counter 而是一個佇列,所以 x.wait() 類似把 process 放到 waiting queue (enqueue),而 signal 則是在 dequeue,所以如果 queue 是空的,signal 就沒有任何作用
- Monitor 可以有很多個 threads 存在,但一次只有一個 active
![](https://i.imgur.com/FMpiMLI.png)
### Dining Philosophers Example
#### monitor type
- 建立三個狀態 `thinking`, `hungry` & `eating`
- 建立 shared variable `self[5]`,存放每個人的狀態
- P.S. 可以用 shared variable 都還算好解決,難解的是無法得知他人狀態的問題
- 定義 4 個 method
```cpp
monitor dp {
enum {thinking, hungry, eating} state[5]; //current state
condition self[5]; //delay eating if can’t obtain chopsticks
void pickup(int i) // pickup chopsticks
void putdown(int i) // putdown chopsticks
void test(int i) // try to eat
void init() {
for (int i = 0; i < 5; i++)
state[i] = thinking;
}
}
```
- void pickup(int i)
- 將狀態設為 `hungry`,代表要吃東西了
- 測試拿筷子後是否可以吃
- 若狀態沒有改變,表示測試失敗,把自己放到 wating queue
```cpp
void pickup(int i) {
state[i] = hungry;
test(i); // try to eat
if (state[i] != eating)
// wait to eat
self[i].wait();
}
```
- void putdown(int i)
- 吃完了將狀態改為 `thinking`
- 告知左右兩邊進行測試,看有沒有辦法吃
```cpp
void putdown(int i) {
state[i] = thinking;
// check if neighbors are waiting to eat
test((i+4) % 5);
test((i+1) % 5);
}
```
- void test(int i)
- 若左右兩邊的人都沒在吃,代表左右兩邊手上都沒筷子,還要確認自己是否想吃東西
- 都符合的話,將狀態改為 `eating` 並喚醒 thread/process
```cpp
// try to let Pi eat (if it is hungry)
void test(int i) {
if ( (state[(i + 4) % 5] != eating) &&(state[(i + 1) % 5] != eating)
&& (state[i] == hungry) ) {
//No neighbors are eating and Pi is hungry
state[i] = eating;
self[i].signal(); // If Pi is suspended, resume it
// If Pi is not suspended, no effect
}
}
```
### Atomic Transactions
- Transactions : 執行一個單一邏輯函式的指令集合
- Atomic Transactions : 執行單一邏輯函式,一次全部執行,或完全不執行
- Atomic Transactions 在 database system 系統中是個非常重要的議題
#### File I/O Example
- Transactions 為一系列讀寫的動作
- 透過 `commit` 以及 `abort` 來確認動作完成
- transaction 成功 : commit --> terminated
- transaction 失敗 : abort --> roll back
- abort 的 transaction 必須回溯,取消所有做過的改變,這個性質的確保為 system 的責任
#### Log-Based Recovery
- 紀錄所有 transaction 的步驟及做過的改變
- Stable storage : 絕對不會丟失儲存的資料
- Write-ahead logging : 基本上 log 紀錄需包含以下資訊
- Transaction name
- Data item name
- Old & new values
- Special events: (Ti starts), (Ti commits)
- Log 用來重建被改動資料的原始狀態
- 利用 `undo(Ti)` 及 `redo(Ti)` 來復原資料
-
#### Checkpoints
Logging 的問題是沒有時間觀念,從系統打開到系統崩潰為止一直紀錄,因此需要 checkpoint 留存系統還原點 (例如強制關機時 word 沒有存檔,重開後系統會問你是否要還原至關機瞬間的狀態),從 checkpoint 來做 roll back 的動作
- 當 failure 發生時,必須查看 log 來確認哪些 transactions 要重新執行
- 搜尋的過程很花時間
- 並非所有 transactions 都需要重新執行
- 利用 checkpoints 來減少上述 overhead
- 輸出所有 log 紀錄來達到 stable storage
- 輸出所有被更動的資料來達到 stable storage
- 輸出紀錄 `checkpoint` 的 log 來達到 stable storage
## Refernece
1. [清大開放課程 - 作業系統](https://www.youtube.com/watch?v=aK0_PBLWCAM&list=PL9jciz8qz_zyO55qECi2PD3k6lgxluYEV&index=38)
2. [Operating System Concept 9th edition](https://www.os-book.com/OS9/slide-dir/index.html)