執行緒函式庫:Pthreads、Windows、Java (Thread Libraries)
本系列文章內容參考自經典教材 Operating System Concepts, 10th Edition (Silberschatz, Galvin, Gagne)。本文對應章節:Section 4.4 Thread Libraries。
4.4 執行緒函式庫 (Thread Libraries)
三大主流函式庫
執行緒函式庫 (Thread Library) 為程式設計者提供一套用來建立與管理執行緒的 API。目前有三大主流函式庫:
| 函式庫 | 標準 / 平台 | 層次 |
|---|---|---|
| POSIX Pthreads | POSIX 標準 (IEEE 1003.1c),廣泛用於 UNIX/Linux/macOS | 可作為 user-level 或 kernel-level 函式庫 |
| Windows Thread API | Windows 系統專用 | Kernel-level 函式庫 |
| Java Thread API | 任何有 JVM 的平台(Windows、Linux、macOS、Android) | 由底層 OS 執行緒函式庫實作 |
Java 的執行緒 API 在 JVM 之上提供統一介面,但底層實作依主機 OS 而不同:Windows 上通常使用 Windows API,Linux/macOS 通常使用 Pthreads。
關於全域資料 (global data) 的共享,Pthreads 與 Windows 執行緒的規則一致:宣告在所有函式外(即全域作用域)的資料,會被同一個進程內的所有執行緒共享,不需要任何額外設定。Java 作為純物件導向語言,沒有「全域變數」的概念,若要在執行緒之間共享資料,必須在設計上明確安排。
兩種多執行緒策略:非同步與同步
在介紹各函式庫的具體 API 之前,先理解兩種基本的執行緒使用模式,因為這兩種模式決定了程式的整體結構。
非同步執行緒 (Asynchronous Threading):父執行緒建立子執行緒後,立即繼續執行,不等待子執行緒完成。兩者並行且相互獨立,幾乎不共享資料。
同步執行緒 (Synchronous Threading):父執行緒建立一條或多條子執行緒後,必須等待所有子執行緒完成才能繼續。子執行緒完成後各自終止並與父執行緒 join(匯合),只有所有子執行緒都 join 之後,父執行緒才得以繼續。同步執行緒通常伴隨大量的資料共享,例如父執行緒最終要匯總所有子執行緒計算出的子結果。
以下三個函式庫的範例程式,全部採用同步執行緒策略。
4.4.1 Pthreads
規格 vs 實作
Pthreads 是 POSIX 標準(IEEE 1003.1c)定義的執行緒 API 規格 (specification),而非一個具體的實作。OS 設計者可以自由選擇如何實作這份規格,因此 Pthreads 程式碼在 Linux、macOS、BSD 等系統上都能運行,底層實 作各異但介面完全相同。Windows 原生不支援 Pthreads,但有第三方實作可用。
建立執行緒:完整範例
以計算 1 到 N 總和為例,設計一個將加總工作交給子執行緒執行的 Pthreads 程式:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int sum; /* 所有執行緒共享的全域變數 */
void *runner(void *param); /* 子執行緒將執行這個函式 */
int main(int argc, char *argv[])
{
pthread_t tid; /* 執行緒識別碼 */
pthread_attr_t attr; /* 執行緒屬性 */
pthread_attr_init(&attr); /* 使用預設屬性 */
pthread_create(&tid, &attr, runner, argv[1]); /* 建立子執行緒 */
pthread_join(tid, NULL); /* 等待子執行緒結束 */
printf("sum = %d\n", sum);
}
void *runner(void *param)
{
int i, upper = atoi(param);
sum = 0;
for (i = 1; i <= upper; i++)
sum += i;
pthread_exit(0); /* 子執行緒結束 */
}
逐步拆解這段程式的執行流程:
main()開始執行,此時只有一條執行緒(父執行緒)在運行pthread_attr_init(&attr)初始化執行緒屬性為預設值(預設屬性已足夠大多數用途;Chapter 5 會討論進階排程屬性)pthread_create(&tid, &attr, runner, argv[1])建立子執行緒:- 第一個參數
&tid:子執行緒的識別碼,建立後填入 - 第二個參數
&attr:執行緒屬性 - 第三個參數
runner:子執行緒從哪個函式開始執行 - 第四個參數
argv[1]:傳入runner()的參數(命令列輸入的 N 值)
- 第一個參數
- 子執行緒在
runner()中計算加總,父執行緒在main()呼叫pthread_join(tid, NULL)後阻塞等待 - 子執行緒計算完成後呼叫
pthread_exit(0)結束,pthread_join()返回,父執行緒繼續輸出結果
此時進程中有兩條執行緒:父執行緒在 main() 中等待,子執行緒在 runner() 中計算。兩者共享全域變數 sum,這正是同步執行緒資料共享的典型用法。
等待多條子執行緒
若要等待多條子執行緒,只需將 pthread_join() 包在一個迴圈中:
#define NUM_THREADS 10
pthread_t workers[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++)
pthread_join(workers[i], NULL);
這段程式依序等待所有子執行緒結束。所有執行緒完成後,父執行緒才 繼續執行後續邏輯。
4.4.2 Windows 執行緒 (Windows Threads)
Windows Thread API 的設計哲學與 Pthreads 相近,只是 API 名稱與資料型別遵循 Windows 的命名慣例(全大寫的型別名稱、Handle 概念等)。以下是同樣計算 1 到 N 加總的 Windows 版本:
#include <windows.h>
#include <stdio.h>
DWORD Sum; /* 共享全域變數,DWORD = unsigned 32-bit integer */
DWORD WINAPI Summation(LPVOID Param)
{
DWORD Upper = *(DWORD*)Param;
for (DWORD i = 1; i <= Upper; i++)
Sum += i;
return 0;
}
int main(int argc, char *argv[])
{
DWORD ThreadId;
HANDLE ThreadHandle;
int Param = atoi(argv[1]);
ThreadHandle = CreateThread(
NULL, /* 預設安全屬性 */
0, /* 預設堆疊大小 */
Summation, /* 執行緒函式 */
&Param, /* 傳入執行緒函式的參數 */
0, /* 預設建立旗標(立即可執行) */
&ThreadId); /* 傳回執行緒 ID */
WaitForSingleObject(ThreadHandle, INFINITE); /* 等待子執行緒結束 */
CloseHandle(ThreadHandle); /* 釋放 handle 資源 */
printf("sum = %d\n", Sum);
}
幾個與 Pthreads 的對應關係:
| Pthreads | Windows API | 用途 |
|---|---|---|
pthread_create() | CreateThread() | 建立執行緒 |
pthread_join() | WaitForSingleObject() | 等待單一執行緒 |
pthread_exit() | return 0(從執行緒函式返回) | 終止執行緒 |
pthread_t(識別碼) | HANDLE(核心物件句柄) | 執行緒的引用 |
CreateThread() 接收一組屬性,包含安全資訊、堆疊大小、起始旗標(例如可設定執行緒以暫停狀態啟動)。本範例使用預設值,執行緒建立後立即成為可被 CPU 排程的狀態。
等待多條子執行緒
在需要等待多條執行緒時,Windows 提供 WaitForMultipleObjects(),它接收四個參數:
- 要等待的物件數量
- 指向
HANDLE陣列的指標 - 旗標:是否要等待所有物件都被觸發(
TRUE)還是任一個即可(FALSE) - 逾時時間(毫秒)或
INFINITE
WaitForMultipleObjects(N, THandles, TRUE, INFINITE);
這行程式會阻塞,直到 THandles 陣列中的 N 條執行緒全部完成為止。