執行緒 (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)。

特性 Process I/O Multiplexing Thread
排程者 kernel 自動 程式設計師手動 kernel 自動
位址空間 各自私有 單一 process 共享 單一 process 共享
kernel 是否可識別流 是 (PID) 是 (TID)
資料共享難易 困難(需 IPC) 容易 容易
context switch 成本 無(單一 process) 低於 process
能否利用多核心 不可
記憶法:thread「像 process」的地方是 kernel 自動排程且有整數 ID;「像 I/O multiplexing」的地方是共享單一 process 的完整位址空間。
例外:雖然 12.2.2 列出 event-driven 設計的缺點(程式碼複雜度高、無法充分利用多核心),現代高效能伺服器如 Node.js、nginx、Tornado 仍採用 I/O multiplexing 的事件驅動設計,因為其效能優勢顯著 (p.1021 Aside)。參見 12-Concurrent-Programming/01-Process-and-Event-Based-Concurrency

12.3.1 Thread Execution Model(執行緒執行模型)(p.1022-1023)

每個 process 一開始都是單一執行緒,稱為 main thread。main thread 在某時點建立 peer thread,之後兩者並行執行。控制權透過 thread context switch 轉移——可能因為 main thread 執行慢速 system call(如 readsleep),或被系統的 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 執行模型的重要差異:

「無父子階層」是考試常考對比點:process 用 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;
}

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 */

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 */
main thread 呼叫 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 會:

  1. 阻塞 (block) 直到 thread tid 終止;
  2. 把 thread routine 回傳的 generic (void *) pointer 存入 thread_return 所指位置;
  3. 回收 (reap) 已終止 thread 持有的所有記憶體資源。
(p.1025)。與 08-Exceptional-Control-Flow/04-Process-Controlwait/waitpid 對照記憶。

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 */

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 */

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);   /* 傳區域變數位址:錯! */

議題 2:避免 thread routine 的 memory leak

Practice Problem 12.5 (p.1028) 考點:process 版伺服器中刪掉 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 會全面回收