執行緒 (Threads)
Overview Table
| 主題 | 核心內容 | 書頁 |
|---|---|---|
| 執行緒定義 | thread = 在 process context 中執行的邏輯流;混合 process 與 I/O multiplexing 兩種模型的優點 | p.1021-1022 |
| 執行模型 | main thread 建立 peer thread;由 kernel 自動排程;thread context switch 比 process 快;無父子階層,是 pool of peers | p.1022-1023 |
| Posix Threads | Pthreads 標準介面(1995),約 60 個函式,所有 Linux 系統皆支援 | p.1023 |
| 建立 | pthread_create(thread routine 為 void *f(void *));pthread_self 取得自身 TID |
p.1024 |
| 終止 | 四種方式:routine return / pthread_exit / exit(殺整個 process)/ pthread_cancel |
p.1024-1025 |
| 回收 | pthread_join 阻塞等待「特定」執行緒終止並回收資源;不能等任意執行緒(與 wait 不同) |
p.1025 |
| 分離 | joinable vs detached;預設 joinable;pthread_detach 讓系統自動回收資源 |
p.1025-1026 |
| 初始化 | pthread_once + PTHREAD_ONCE_INIT:動態初始化共享全域變數,只執行一次 |
p.1026 |
| 執行緒版伺服器 | echoservert.c:每個連線一個 detached peer thread;connfd 必須 Malloc 傳遞以避免 race |
p.1027-1028 |
12.3 引言:第三種並行模型 (p.1021-1022)
前兩種建立並行邏輯流的方法各有極端:process 由 kernel 自動排程,但各自擁有私有位址空間,流之間難以共享資料;I/O multiplexing 在單一 process 中自建邏輯流,共享整個位址空間,但必須由程式設計師手動排程。執行緒 (thread) 是兩者的混合體 (hybrid)。
- thread 是在 process context 中執行的邏輯流;目前為止書中所有程式都是每個 process 單一執行緒。
- 每個 thread 有自己的 thread context:唯一整數 thread ID (TID)、stack、stack pointer、program counter (PC)、通用暫存器、condition codes。
- 同一 process 內所有 threads 共享整個虛擬位址空間:code、data、heap、shared libraries、open files。
| 特性 | Process | I/O Multiplexing | Thread |
|---|---|---|---|
| 排程者 | kernel 自動 | 程式設計師手動 | kernel 自動 |
| 位址空間 | 各自私有 | 單一 process 共享 | 單一 process 共享 |
| kernel 是否可識別流 | 是 (PID) | 否 | 是 (TID) |
| 資料共享難易 | 困難(需 IPC) | 容易 | 容易 |
| context switch 成本 | 高 | 無(單一 process) | 低於 process |
| 能否利用多核心 | 可 | 不可 | 可 |
12.3.1 Thread Execution Model(執行緒執行模型)(p.1022-1023)
每個 process 一開始都是單一執行緒,稱為 main thread。main thread 在某時點建立 peer thread,之後兩者並行執行。控制權透過 thread context switch 轉移——可能因為 main thread 執行慢速 system call(如 read、sleep),或被系統的 interval timer 中斷。
時間 Thread 1 (main thread) Thread 2 (peer thread)
| |
| | ← main thread 執行
| +--- pthread_create --------+
| | |
| | (slow syscall / timer) |
| +====== context switch ====>| ← peer thread 執行
| |
| |<===== context switch =====+
| |
| +====== context switch ====>|
v |
與 process 執行模型的重要差異:
- context switch 較快:thread context(暫存器 + stack)遠小於 process context(需含 page table、open file table 等),切換成本低。
- 無嚴格父子階層:threads 不像 processes 有 parent-child hierarchy;同一 process 的 threads 形成 pool of peers(對等池),與誰建立誰無關。
- main thread 唯一的特殊之處:它永遠是 process 中第一個執行的 thread。
- pool of peers 的影響:任一 thread 可以 kill 任何 peer、等待任何 peer 終止,且每個 peer 都能讀寫相同的共享資料。
wait/waitpid 時只有 parent 能回收 child;thread 則任何 peer 都能 pthread_join 任何其他 peer。12.3.2 Posix Threads (Pthreads) (p.1023)
Pthreads 是在 C 程式中操作執行緒的標準介面,1995 年被採納,所有 Linux 系統皆可用。定義約 60 個函式,功能涵蓋:建立、殺死、回收 (reap) threads、與 peers 安全共享資料、通知 peers 系統狀態變化。
最小範例 hello.c (Figure 12.13):
#include "csapp.h"
void *thread(void *vargp);
int main()
{
pthread_t tid;
Pthread_create(&tid, NULL, thread, NULL);
Pthread_join(tid, NULL);
exit(0);
}
void *thread(void *vargp) /* Thread routine */
{
printf("Hello, world!\n");
return NULL;
}
- thread 的程式碼與區域資料封裝在 thread routine 中,原型固定為
void *routine(void *vargp):單一 generic pointer 輸入、generic pointer 回傳。 - 要傳多個參數:把參數包進 structure,傳該 structure 的指標。
- 要回傳多個結果:回傳指向 structure 的指標。
- main thread 呼叫
exit(0)會終止 process 中所有 threads。
12.3.3 建立執行緒 (p.1024)
#include <pthread.h>
typedef void *(func)(void *);
int pthread_create(pthread_t *tid, pthread_attr_t *attr,
func *f, void *arg);
/* Returns: 0 if OK, nonzero on error */
pthread_t pthread_self(void);
/* Returns: thread ID of caller */
pthread_create建立新 thread,在新 thread 的 context 中執行 routinef,輸入參數為arg。attr可改變新 thread 的預設屬性;本書範例一律傳NULL。pthread_create返回時,tid已存入新 thread 的 ID,且 main thread 與新 peer thread 已並行執行。- 新 thread 可呼叫
pthread_self取得自己的 TID。
12.3.4 終止執行緒 (p.1024-1025)
一個 thread 以下列四種方式之一終止:
| 方式 | 影響範圍 | 說明 |
|---|---|---|
| top-level thread routine return | 只終止該 thread | 隱式 (implicit) 終止 |
呼叫 pthread_exit(void *thread_return) |
只終止該 thread(main thread 例外,見下) | 顯式終止;never returns |
某 peer thread 呼叫 Linux exit |
整個 process 及所有 threads | 全滅 |
其他 peer 呼叫 pthread_cancel(pthread_t tid) |
終止 TID 為 tid 的 thread | Returns: 0 if OK, nonzero on error |
#include <pthread.h>
void pthread_exit(void *thread_return); /* Never returns */
int pthread_cancel(pthread_t tid); /* Returns: 0 if OK, nonzero on error */
pthread_exit 的特殊行為:它會先等待所有其他 peer threads 終止,然後才終止 main thread 與整個 process,回傳值為 thread_return。這與 main thread 呼叫 exit(立即終止所有 threads)截然不同。12.3.5 回收已終止的執行緒 (p.1025)
#include <pthread.h>
int pthread_join(pthread_t tid, void **thread_return);
/* Returns: 0 if OK, nonzero on error */
pthread_join 會:
- 阻塞 (block) 直到 thread
tid終止; - 把 thread routine 回傳的 generic
(void *)pointer 存入thread_return所指位置; - 回收 (reap) 已終止 thread 持有的所有記憶體資源。
12.3.6 分離執行緒 (p.1025-1026)
任一時刻,thread 是 joinable(可接合) 或 detached(已分離) 兩者之一:
pthread_create (預設)
|
v
+------------------+ pthread_detach(tid) +------------------+
| joinable | ----------------------> | detached |
+------------------+ +------------------+
可被其他 thread 不可被其他 thread
reap 與 kill; reap 或 kill;
記憶體資源需被 終止時系統「自動」
另一 thread reap 釋放記憶體資源
後才釋放
| joinable | detached | |
|---|---|---|
| 可被其他 thread reap/kill | 可 | 不可 |
| 記憶體資源(如 stack)釋放時機 | 被另一 thread reap 時 | 終止時由系統自動釋放 |
| 預設 | 是(threads 建立時預設 joinable) | 否 |
#include <pthread.h>
int pthread_detach(pthread_t tid);
/* Returns: 0 if OK, nonzero on error */
- 為避免 memory leak,每個 joinable thread 必須「被另一 thread 顯式 reap」或「被
pthread_detach分離」,二擇一。 - thread 可自我分離:
pthread_detach(pthread_self())。 - 實務上偏好 detached threads:例如高效能 Web server 對每個連線請求建立一個 peer thread,各連線獨立處理,server **不需要(也不應該)**逐一等待 peer 終止;每個 peer thread 應在開始處理請求前先自我分離,終止後資源即可自動回收。
12.3.7 初始化執行緒 (p.1026)
#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control,
void (*init_routine)(void));
/* Always returns 0 */
once_control是全域或 static 變數,必須初始化為PTHREAD_ONCE_INIT。- 第一次以某
once_control呼叫pthread_once時,執行init_routine(無參數、無回傳值的函式);之後以同一once_control呼叫則什麼都不做。 - 用途:需要動態初始化被多個 threads 共享的全域變數時(範例見 12.5.5,12-Concurrent-Programming/03-Shared-Variables-and-Semaphores)。
12.3.8 執行緒版並行伺服器 (p.1027-1028)
echoservert.c (Figure 12.14) 的整體結構與 process 版類似:main thread 反覆等待連線請求,再建立 peer thread 處理。
main thread peer thread (每連線一個)
----------- ------------------------
Open_listenfd(port)
|
v
+->Accept() ---> connfdp = Malloc(sizeof(int))
| | *connfdp = connfd
| v
| Pthread_create(&tid, NULL,
| thread, connfdp) ----> connfd = *((int *)vargp)
| | Pthread_detach(pthread_self())
+------+ (立刻回到 Accept) Free(vargp)
echo(connfd)
Close(connfd)
return NULL
兩個關鍵而微妙的正確性議題:
議題 1:如何把 connected descriptor 傳給 peer thread —— race 陷阱
直覺寫法(錯誤):
connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
Pthread_create(&tid, NULL, thread, &connfd); /* 傳區域變數位址:錯! */
- 這在「peer thread 的賦值
int connfd = *((int *)vargp);」與「main thread 的下一次accept」之間引入 race(競爭)。 - 若賦值先於下一次 accept 完成 → peer 拿到正確 descriptor。
- 若賦值晚於 accept 完成 → peer 拿到的是下一個連線的 descriptor,導致兩個 threads 對同一 descriptor 做 I/O。
- 正解:每個
accept回傳的 descriptor 都放進自己專屬的動態配置記憶體區塊(connfdp = Malloc(sizeof(int)); *connfdp = Accept(...))。race 議題將在 12.7.4 深入(12-Concurrent-Programming/04-Parallelism-and-Thread-Safety)。
議題 2:避免 thread routine 的 memory leak
- 因為不顯式 reap threads,每個 thread 必須
Pthread_detach(pthread_self()),終止時資源才會被回收。 - 也必須
Free(vargp)釋放 main thread 為 connfd 配置的記憶體區塊。
Close(connfd)(子行程那行)不會漏,因為子行程 exit 時 kernel 會關閉其所有 descriptor;但 thread 版中刪掉 Pthread_detach(行 31)→ joinable thread 無人 reap,記憶體資源(stack)洩漏;刪掉 Free(vargp)(行 32)→ Malloc 的區塊洩漏。threads 共享同一 process,thread 終止不會觸發整個 process 的資源回收。Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
| thread context 包含哪些? | TID、stack、stack pointer、PC、通用暫存器、condition codes(不含 code/data/heap/open files——那些是共享的) |
| threads 共享哪些? | 整個 process 虛擬位址空間:code、data、heap、shared libraries、open files |
| thread vs process 的 context switch | thread context 較小 → thread context switch 較快 |
| threads 有父子階層嗎? | 無,是 pool of peers;任何 peer 可 kill/join 任何其他 peer;main thread 只是「第一個執行」 |
| 傳多個參數給 thread routine | 包成 structure,傳 structure 指標 |
| thread 終止的四種方式 | routine return / pthread_exit / exit(全 process)/ pthread_cancel |
main thread 呼叫 pthread_exit |
等所有 peers 終止後,才終止 main thread 與整個 process |
某 peer thread 呼叫 exit |
立即終止 process 與所有 threads |
pthread_join 能等任意 thread 嗎? |
不能,只能等特定 tid(與 wait 不同;Stevens 認為是規格 bug) |
| joinable thread 沒人 join 也沒 detach | memory leak(stack 等資源不會釋放) |
| Web server 該用 joinable 還是 detached? | detached:各連線獨立,不需等待 peer,終止即自動回收 |
pthread_once 第二次呼叫(同一 once_control) |
什麼都不做;用於一次性初始化共享全域變數 |
Pthread_create(&tid, NULL, thread, &connfd) 傳區域變數位址 |
race:peer 的解參考可能晚於 main 的下一次 accept → 兩 threads 用同一 descriptor;必須為每個 connfd 各自 Malloc |
| thread 版 echo server 刪掉 detach 或 free | 各造成 memory leak(P.P. 12.5);thread 終止不像 process exit 會全面回收 |
Related Notes
- 12-Concurrent-Programming/01-Process-and-Event-Based-Concurrency
- 12-Concurrent-Programming/03-Shared-Variables-and-Semaphores
- 12-Concurrent-Programming/04-Parallelism-and-Thread-Safety
- 08-Exceptional-Control-Flow/02-Processes-and-Context-Switches
- 08-Exceptional-Control-Flow/04-Process-Control
- 11-Network-Programming/03-Sockets-Interface