前幾天我們花了很多時間理解 Process。
一個 Process 有自己的 Virtual Address Space、系統資源與執行環境,而 CPU 又可以透過 Context Switch,在不同的執行工作之間切換。
既然 Process 已經可以讓不同工作同時進行,那為什麼還需要 Thread?
假設一個Server
│
├── 處理 User A
├── 處理 User B
├── 處理 User C
└── 處理 User D
我們當然可以建立很多 Process。
但如果**這些工作本來就屬於同一個程式,而且還需要頻繁使用共同的資料,全部拆成獨立 Process 不一定是最方便的方式。**於是我們開始需要:
CPU 真正執行的是一連串 Instruction,而要讓這串 Instruction 能夠執行下去,需要知道:
假設今天程式裡同時有很多事情要處理:播放音樂 / 更新 UI / 讀取使用者操作 / 下載下一首歌曲資訊
如果全部只有一條 Thread:
[Thread 1]
讀取網路資料
│
│ 等待......
│
▼
更新 UI
│
▼
處理使用者輸入
其中一個工作如果因為某些操作而等待,其他工作也可能受到影響。
Process
│
├── Thread 1 → 處理 UI
│
├── Thread 2 → 處理網路
│
└── Thread 3 → 處理其他工作
再來,Multi-thread 不等於一定「同時」執行,假設只有一個 CPU Core:
時間 ───────────────────────►
Thread A
██████
Thread B
██████
Thread A
██████
假設:
Process A
│
├── Thread 1
├── Thread 2
└── Thread 3
它們雖然有不同的執行流程,但仍然屬於同一個 Process。
因此它們通常共享 Process 層級的資源,其中最重要的就是:Virtual Address Space
也就是說,Thread 1 和 Thread 2 通常看到的是同一套 Process Address Space。
這和 fork() 非常不同。
Day 6 的 Parent 和 Child:
Parent Process
│
└── Virtual Address Space A
Child Process
│
└── Virtual Address Space B
即使剛 fork() 完成時底層可能透過 COW 暫時共享 Physical Pages,從程式的角度來看,兩個 Process 仍然具有彼此獨立的 private address space。
但是 Thread:
Process
│
Virtual Address Space
│
┌────────┼────────┐
│ │ │
▼ ▼ ▼
Thread 1 Thread 2 Thread 3
因此它們通常可以直接使用共同的:Code / Global / Static Data / Heap / Memory Mapping
除此之外,屬於 Process 的許多資源也會被 Threads 共同使用。
而這件事情既是優點,也是後面最大的麻煩來源。
假設 Thread 1 執行:
void functionA() {
int x = 10;
functionB();
}
(1)當 Function Call 發生時,Stack 上可能需要保存與這條執行流程相關的資訊,例如:
Thread 1 Stack
┌─────────────────────┐
│ functionB Stack Frame│
├─────────────────────┤
│ functionA Stack Frame│
├─────────────────────┤
│ main Stack Frame │
└─────────────────────┘
裡面可能包含:Local Variables / Return Address / Saved Registers / Function Call 相關資訊
(2)但 Thread 2 可能同時正在執行完全不同的函式:
void download() {
int progress = 50;
...
}
它自己的呼叫路徑可能是:
Thread 2 Stack
┌──────────────────────┐
│ download Stack Frame │
├──────────────────────┤
│ worker Stack Frame │
├──────────────────────┤
│ start Stack Frame │
└──────────────────────┘
因此:每個 Thread 必須擁有自己的 Stack,因為 Stack 本身就是這條 execution flow 的一部分。
先講獨立address space 的process,假設有兩個 Process:
Process A:Memory A
Process B:Memory B
因為 Address Space 原則上彼此隔離,如果 A 想把資料交給 B,通常不能直接說:
B_variable = 100;
它們需要透過某種 **IPC(Inter-Process Communication) 機制。**行程間通訊/程序間通訊。
:IPC 是讓彼此隔離的 Process 能夠透過受控制的方式交換資料或協調工作的機制。
不同 Process 原本有 Address Space Isolation,因此它們需要 IPC 才能以受控制的方式交換資料或協調;而同一個 Process 裡的 Threads 則天然共享 Process Address Space,所以共享資料通常比較直接可得。
問題就在於:兩條 Thread 可以直接碰到同一份 Memory,而且執行順序可能交錯。
這就是之後會遇到的:Race Condition
所以 Multi-threading 最大的優勢和最大的麻煩,其實來自同一件事:Shared Address Space
Shared Data<-Thread A / Thread B
同時也代表:
同時修改?Thread A / Thread B
↓
Race Condition
所以它只是提供另一種 concurrency model,而你必須處理共享狀態所帶來的 synchronization 問題。
Process 提供 Resource / Protection Boundary,而 Thread 提供 Execution Flow。
兩條 Thread 屬於同一個 Process,通常不需要切換到完全不同的 Process Address Space,所以 Thread Switch 常常可以比跨 Process 的切換少掉一些工作,但實際成本還是會受到 OS、CPU architecture、Cache、Scheduler 與 workload 等因素影響。
1.一個 Process 提供:
而 Process 裡可以存在多條 Thread:
Process
│
Shared Address Space
│
┌─────────┴─────────┐
│ │
Thread A Thread B
│ │
own PC own PC
own Registers own Registers
own Stack own Stack
2.不同 Process 因為 Address Space 彼此隔離,如果需要合作,通常必須使用:
IPC
│
├── Pipe
├── Socket
├── Shared Memory
└── Message Queue
讓資料可以在 Process 邊界之間受控制地傳遞。
而同一個 Process 裡的 Threads 則天然共享 Address Space,因此資料交換更加直接。
3.但也正因如此:
共享 Memory
│
├── 優點:溝通方便
│
└── 代價:可能同時存取同一份資料
│
▼
Race Condition
假設兩條 Thread 同時執行:counter++;
明明只是「加一」而已,為什麼最後的答案竟然可能算錯?
下一篇: