並行程式設計練習題 (Practice - Concurrent Programming)
Related Concepts
- 12-Concurrent-Programming/01-Process-and-Event-Based-Concurrency
- 12-Concurrent-Programming/02-Threads
- 12-Concurrent-Programming/03-Shared-Variables-and-Semaphores
- 12-Concurrent-Programming/04-Parallelism-and-Thread-Safety
| 關鍵字 | 答案 |
|---|---|
| 三種並行途徑的排程者/位址空間 | Processes:kernel/私有;I/O multiplexing:應用程式/共享;Threads:kernel/共享 |
| parent 不 close connfd | file table entry refcount 永不歸零 → memory leak → 最終 crash |
| SIGCHLD handler 用 while waitpid | Linux signals 不排隊,一個 pending signal 可能對應多個 zombie |
| descriptor ready for reading | 對它讀 1 byte 的請求不會阻塞 |
| select 的副作用 | 就地把 fdset 改寫成 ready set → 每輪要 ready_set = read_set |
| thread context 內容 | TID、stack、stack pointer、PC、通用暫存器、condition codes |
| threads 共享什麼 | code、data、heap、shared libraries、open files(整個位址空間) |
| main thread 呼叫 pthread_exit | 先等所有 peers 終止,再終止整個 process |
| 變數是 shared 的定義 | 其某個 instance 被多於一個 thread 參考 |
| cnt++ 為何會錯 | 拆成 Load-Update-Store;L2 落在 L1 與 S1 之間 → lost update |
| semaphore invariant | 正確初始化的 semaphore 值永不為負 |
| Sbuf 三個 semaphore 初值 | mutex = 1、slots = n、items = 0 |
| rand / strtok(Class 2) | 跨呼叫保留 static state;只能改寫成 reentrant(_r 版本) |
| ctime / gethostbyname(Class 3) | 回傳 static 指標;lock-and-copy 或 _r 版本 |
| reentrant vs thread-safe | reentrant 是 thread-safe 的 proper subset |
| speedup / efficiency | |
| 兩 thread 以不同順序 P 兩個 mutex | forbidden regions 重疊 → deadlock region;用 lock ordering rule 修 |
傳 &i 給 pthread_create |
race:main 的 i++ vs peer 的解參考;改用 Malloc 專屬區塊 |
Question 1 - 三種並行途徑 [recall]
現代 OS 提供三種建構並行程式的基本途徑:Processes、I/O multiplexing、Threads。
請分別說明:各由誰負責排程?邏輯流之間的位址空間是共享還是私有?為何說 Threads 是「混合體」?
Processes:kernel 自動排程,各自擁有私有的 virtual address space(共享須顯式 IPC)。I/O multiplexing:應用程式自行顯式排程,所有邏輯流在單一行程內共享整個位址空間。Threads:kernel 自動排程,且在單一行程內共享位址空間。
Threads 是混合體:「像 process」的地方是 kernel 自動排程、有整數 TID;「像 I/O multiplexing」的地方是共享單一 process 的完整位址空間。
Question 2 - Thread Context 與共享 [recall]
同一個 process 內的多個 threads,哪些東西屬於各 thread 私有的 thread context?哪些是所有 threads 共享的?
私有(thread context):TID、stack、stack pointer、PC、通用暫存器、condition codes。
共享:整個 process 的 virtual address space——code(text)、data、heap、shared libraries,以及 open files。
注意:registers 永不共享,virtual memory 永遠共享;thread stack 只是「通常」私有,並無硬體保護。
Question 3 - Thread 終止方式 [recall]
列出 thread 終止的四種方式,並說明:main thread 呼叫
pthread_exit與呼叫exit的行為差異。
四種方式:(1) top-level thread routine return(隱式);(2) 呼叫 pthread_exit(顯式);(3) 某 peer 呼叫 Linux exit——終止整個 process 及所有 threads;(4) 其他 peer 呼叫 pthread_cancel(tid)。
main thread 呼叫 pthread_exit 會先等待所有其他 peer threads 終止,才終止 main thread 與整個 process;呼叫 exit 則立即終止所有 threads。
Question 4 - select 的語意與副作用 [recall]
使用
select撰寫 I/O multiplexing 伺服器時:
(a) 「描述子 k ready for reading」的精確定義是什麼?
(b) 為何每輪迴圈都必須先執行ready_set = read_set;再呼叫select?
(a) 對描述子 k 讀取 1 byte 的請求不會阻塞,即為 ready for reading。
(b) select 有副作用:它會就地修改 fdset,把 read set 改寫成 ready set(read set 中已 ready 的子集),回傳值即 ready set 的 cardinality。若不每輪還原,read set 會逐漸只剩上一輪 ready 的描述子——這是經典 bug。
Question 5 - Shared Variable 的判定 [recall]
CSAPP 對「shared variable」給出精確定義。請寫出該定義,並說明 global、local static、local automatic 三種變數在 run time 各有幾份 instance。
定義:變數 v 是 shared ⟺ v 的某一個 instance 被多於一個 thread 參考。
Global 與 local static:恰好 1 份 instance,位於 read/write data 區;local automatic:每個 thread 的 stack 各 1 份。
注意:local automatic 也可能是 shared——例如透過 global 指標間接參考他人 stack 上的陣列(sharing.c 的 msgs)。
Question 6 - P/V 操作與 Semaphore Invariant [recall]
說明 Dijkstra semaphore 的 P(s) 與 V(s) 操作語意(含 s = 0 時 P 的行為、V 重啟等待者的規則),並寫出 semaphore invariant。
P(s):若 s 非零,不可分割地遞減 s 後返回;若 s 為零,掛起該 thread,直到 s 變非零且被某個 V 重啟,重啟後完成遞減再返回。
V(s):不可分割地把 s 加 1;若有 threads 阻塞在 P,恰好重啟其中一個,但重啟順序未定義、不可預測。
Semaphore invariant:執行中的程式永遠不會使正確初始化的 semaphore 變成負值。
Question 7 - Readers-Writers 與 Starvation [recall]
Readers-writers problem 有 first(偏袒 readers)與 second(偏袒 writers)兩種變體。請說明兩者的規則差異,以及為何說「兩種正確解都可能導致 starvation」。
First:除非 writer 已獲准使用物件,reader 不等待;reader 不因「有 writer 在等」而等。Second:writer 就緒後盡快寫,晚於 writer 到達的 reader 必須等,即使該 writer 也在等待。
Starvation:某 thread 無限期阻塞。例如 first 解中 readers 連續到達可讓 writer 無限期等待;且因 V(w) 重啟哪個等待者不可預測,一串 writers 也可能餓死 reader。
Question 8 - 四類 Thread-Unsafe Functions [recall]
列出四類 thread-unsafe functions,各舉一例,並指出哪一類「只能改寫、無法用鎖補救」。
Class 1:不保護共享變數(如遞增未保護的全域 counter)→ 用 P/V 保護。Class 2:跨呼叫保留 static state(如 rand、strtok)→ 唯一解法是改寫成 reentrant(caller 以參數傳入 state),lock-and-copy 無效。Class 3:回傳指向 static variable 的指標(如 ctime、gethostbyname)→ lock-and-copy 或 _r 版本。Class 4:呼叫 thread-unsafe function 的函式——若被呼叫者是 Class 2 則 f 也 unsafe;若是 Class 1/3,以 mutex 保護 call site 即可 safe。
Question 9 - Reentrant vs Thread-Safe [recall]
說明 reentrant function 的定義、它與 thread-safe functions 的集合關係,以及 explicitly reentrant 與 implicitly reentrant 的差別。
Reentrant function:被多條 threads 呼叫時不引用任何 shared data 的函式;reentrant 是 thread-safe 的 proper subset,且因不需同步操作通常更快。
Explicitly reentrant:所有參數 pass by value 且只存取區域自動變數——僅看程式碼即可判定。Implicitly reentrant:允許部分參數傳指標(如 rand_r),僅當 caller 傳入非共享資料的指標時才 reentrant——是 caller 與 callee 共同的屬性。
Question 10 - connfd Refcount 追蹤 [application]
Process-based echo server 中(Figure 12.5),listenfd = 3、
accept回傳 connfd = 4。
追蹤 connfd 對應 file table entry 的 reference count:從accept回傳、fork、parentClose(connfd)、到 child 服務完關閉,各是多少?若 parent 忘了Close(connfd)會發生什麼事?
accept 回傳:0→1;fork(描述子表複製、指向同一 file table entry):1→2;parent Close(connfd):2→1;child 端關閉(或 child exit 時 kernel 自動關閉):1→0,連線才真正終止。
parent 不 close:長駐的 parent 永不終止,該 entry refcount 永不歸零、永不釋放 → memory leak,最終耗盡記憶體使系統 crash。child 那行 close 可省,因為行程終止時 kernel 會自動關閉所有 open descriptors。
Question 11 - Speedup 與 Efficiency 計算 [application]
某平行程式循序版在單核執行時間
秒;平行版在 4 核執行 秒、在 8 核執行 秒。
計算,並說明 absolute speedup 與 relative speedup 哪個較能真實反映平行化效益。
Absolute speedup(
Question 12 - Sbuf Semaphore 初值與 n=1 特例 [application]
Producer-consumer 的
sbuf_t使用三個 semaphores:mutex、slots、items。
(a) 三者的初始值各是多少?各扮演什麼角色?
(b) 若 buffer 只有 1 個 slot(n = 1),即使有多個 producers 與 consumers,mutex是否仍必要?為什麼?
(a) mutex 初始 1(binary semaphore,保護 buf 等共享狀態的互斥);slots 初始 n(counting,計數空 slot);items 初始 0(counting,計數可用 item)。insert:P(slots)→P(mutex)→放入→V(mutex)→V(items);remove 對稱。
(b) 不必要。n = 1 時 slots(初值 1)/items 已保證任一時刻只有一方能進入 buffer(強制交替存取),即使 p>1、c>1 亦然。事實上 PP 12.9 的三種情境(含 p=1/c=1/n>1)皆不需要 mutex——單一 producer 只改 rear、單一 consumer 只改 front,寫入的空 slot 與讀取的滿 slot 必為不同索引;決定因素是「單生產者/單消費者」或 n=1 的序列化,而非 n 值本身。
Question 13 - cnt++ 的 Interleaving 分析 [analysis]
兩個 threads 各執行
cnt++(共享volatile long cnt,初值 0)一次。cnt++被編譯為三步:(load 到 %rdx )、 (加 1)、 (寫回)。
考慮執行順序:逐步追蹤 %rdx1、%rdx2 與 cnt 的值,判斷最終結果是否正確,並給出判斷任意 interleaving 正確與否的一般準則。
一般準則:只看 L、U、S 三段,若某 thread 的 L 落在另一 thread 的 L 與 S 之間,結果必錯;兩個 critical sections 完全不交錯才正確。而 OS 選哪種排程順序不可預測,故必須以同步(如 mutex)強制互斥。
Question 14 - Deadlock 的 Progress Graph 分析 [analysis]
兩個 threads 使用兩個初始值皆為 1 的 mutexes s、t:
/* Thread 1 */ /* Thread 2 */ P(s); P(t); P(t); P(s); /* critical section */ /* critical section */ V(s); V(t); V(t); V(s);用 progress graph 的觀點解釋:為何這段程式可能 deadlock 卻又不一定每次都 deadlock?應如何修正?
兩 threads 以不同順序取得兩個 mutexes,使 s 與 t 的 forbidden regions(s<0 或 t<0 的狀態)在 progress graph 上重疊,重疊處誘發 deadlock region:軌跡一旦進入(Thread 1 拿到 s、Thread 2 拿到 t 的狀態),向右與向上都被 forbidden region 擋住——各自等待對方永遠不會執行的 V,進得去出不來。
不一定死結:有些幸運軌跡繞過 deadlock region(某 thread 一口氣拿到兩把鎖),故 bug 不可預測、難重現。
修正:mutex lock ordering rule——給所有 mutexes 一個 total ordering,每條 thread 依序取得、反序釋放(如兩邊都改成先 P(s) 再 P(t)),兩個 forbidden regions 便不再重疊,程式 deadlock-free。
| 題型 | 答題模式 |
|---|---|
| 三途徑比較 | 兩軸作答:排程者(kernel vs 應用程式)× 位址空間(私有 vs 共享);Threads = 混合體 |
| 描述子 refcount | accept 0→1、fork 1→2、每個 close 減 1;歸零連線才終止;行程 exit 由 kernel 自動關閉 |
| thread context 題 | 私有 = 暫存器類 + stack;共享 = 整個位址空間 + open files;stack 私有只是慣例 |
| shared 判定 | 三步:記憶體模型 → 每變數 instance 數(global/static 1 份、automatic 每 stack 1 份)→ 每 instance 被幾個 threads 參考(含指標間接) |
| interleaving 填表 | 逐步追蹤 %rdx1/%rdx2/cnt;L 夾在他人 L 與 S 之間即錯(lost update) |
| semaphore 初值 | mutex = 1;Sbuf slots = n、items = 0;invariant:值永不為負 |
| thread-unsafe 分類 | 先辨 Class(1 沒鎖 / 2 static state / 3 回傳 static 指標 / 4 呼叫 unsafe);Class 2 只能改寫成 reentrant |
| speedup 計算 | |
| race 題 | 找「正確性依賴到達順序」處;傳 &i/&connfd 型 → Malloc 專屬區塊 + peer Free + detach |
| deadlock 題 | 畫 progress graph 找 forbidden regions 重疊;修法 = mutex lock ordering rule(依序取得、反序釋放) |