多執行緒程式設計的議題 (Threading Issues)
本系列文章內容參考自經典教材 Operating System Concepts, 10th Edition (Silberschatz, Galvin, Gagne)。本文對應章節:Section 4.6 Threading Issues。
4.6 多執行緒程式設計的議題 (Threading Issues)
多執行緒程式設計帶來效能與結構上的好處,但也引入了一系列在單執行緒世界中不存在的複雜問題。本節討論設計多執行緒程式時必須面對的五個核心議題:fork() 與 exec() 的語意變化、信號的傳遞策略、執行緒取消的安全機制、執行緒私有 資料的存放方式,以及 kernel 與執行緒函式庫之間的協調機制。
4.6.1 fork() 與 exec() 的語意變化
在 Chapter 3 中,fork() 系統呼叫的語意很清楚:複製呼叫它的行程,建立一個完整的副本。然而,當行程內部有多個執行緒時,fork() 的行為就變得模糊了。
以一個具體情境為例:行程 P 有三個執行緒(T1、T2、T3),此時 T2 呼叫了 fork()。問題是,新建立的子行程應該:
- 複製所有執行緒(建立含有三個執行緒的子行程)?
- 還是只複製呼叫 fork() 的那個執行緒(建立只有 T2 的子行程)?
兩種選擇各有其用途,UNIX 系統因此提供了兩個版本的 fork():一個複製所有執行緒,另一個只複製呼叫的執行緒。
選擇哪個版本取決於 fork() 之後要做什麼:
| 情境 | 應選用的版本 |
|---|---|
fork() 後立刻呼叫 exec() | 只複製呼叫的執行緒 |
fork() 後不呼叫 exec(),子行程獨立執行 | 複製所有執行緒 |
原因在於 exec() 的行為:exec() 被呼叫後,會用新的程式取代整個行程,包括所有執行緒。如果 fork() 後馬上呼叫 exec(),那麼複製所有執行緒只是無謂的浪費,因為那些執行緒馬上就會被新程式覆蓋掉。反之,如果子行程要獨立執行而不呼叫 exec(),就應該複製所有執行緒,讓子行程保有完整的執行狀態。
4.6.2 信號處理 (Signal Handling)
信號的基本概念
UNIX 系統使用信號 (Signal) 通知行程某個特定事件已發生。信號的整個生命週期遵循固定的三個階段:
依據信號來源,可分為兩類:
同步信號 (Synchronous Signal):由行程自己的動作觸發,例如非法記憶體存取(segmentation fault)或除以零(division by zero)。這類信號產生後,會送回觸發它的那個行程,因此稱為同步:誰造成問題,誰收到信號。
非同步信號 (Asynchronous Signal):由行程外部的事件觸發,例如使用者按下 <Ctrl><C> 中止程式,或計時器到期。這類信號通常被發送給另一個行程。
信號處理器 (Signal Handler)
每個信號都有兩種可能的處理方式:
- 預設信號處理器 (Default Signal Handler):kernel 內建的處理邏輯,例如收到 SIGTERM 就終止行程。
- 使用者定義信號處理器 (User-defined Signal Handler):程式設計者自行撰寫的函式,用來覆蓋 kernel 的預設行為。
在單執行緒程式中,信號處理非常直接:信號一律送給「這個行程」,行程執行對應的 handler 即可。
多執行緒中的信號傳遞難題
問題出在多執行緒程式中:一個行程內有多個執行緒,信號究竟要送給哪一個?
系統設計上有四種選項:
| 選項 | 說明 |
|---|---|
| ① 送給信號適用的那個執行緒 | 例如:某執行緒執行了非法記憶體存取,就由它自己處理 |
| ② 送給行程內的每個執行緒 | 所有執行緒都收到信號 |
| ③ 送給行程內特定幾個執行緒 | 選擇性傳遞 |
| ④ 指定一個專屬執行緒接收所有信號 | 集中處理 |
選擇哪種方式取決於信號類型:
- 同步信號(如非法存取、除以零)應送給