線程技術自 60 年代提出,80 年代中期才真正進入作業系統內核。Linux 採用 POSIX 線程標準,透過 pthread 庫實現。相較於進程,線程共享地址空間,創建/切換開銷極低(約為進程的 1/30),並提供便捷的數據共享機制,是高併發、高響應應用的首選。本文從基本概念出發,逐步深入到屬性設置、同步機制、線程私有數據等進階主題,並提供完整可編譯範例。
| 維度 | 進程 | 線程 |
|---|---|---|
| 地址空間 | 獨立 | 共享 |
| 創建開銷 | 大(複製頁表、建立數據結構) | 小(僅分配棧、寄存器上下文) |
| 切換開銷 | 大(涉及 MMU、TLB 刷新) | 小(用戶/核心態上下文切換) |
| 通信方式 | IPC(管道、Socket、共享內存) | 直接讀寫全域/堆變數 |
| 穩定性 | 隔離性強,崩潰不影響其他進程 | 一線程崩潰導致整個進程退出 |
核心優勢:
gcc example.c -lpthread -o example
pthread_create:創建線程。pthread_join:等待線程結束並回收資源。pthread_exit:主動終止當前線程。pthread_t:線程標識符(unsigned long int)。#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg) {
for (int i = 0; i < 3; ++i) {
printf("子線程 %d\n", i);
sleep(1);
}
return NULL;
}
int main(void) {
pthread_t tid;
int ret = pthread_create(&tid, NULL, thread_func, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
for (int i = 0; i < 3; ++i) {
printf("主線程 %d\n", i);
sleep(1);
}
pthread_join(tid, NULL); // 等待子線程結束
return 0;
}
輸出(交替順序不定,視調度器而定):
主線程 0
子線程 0
主線程 1
子線程 1
主線程 2
子線程 2
屬性對象 pthread_attr_t 需先 pthread_attr_init 初始化,再用專用函數修改,最後傳給 pthread_create,用完需 pthread_attr_destroy 釋放。
PTHREAD_SCOPE_SYSTEM(綁定):線程 1:1 映射到輕量級進程(LWP),響應快,適合實時任務。PTHREAD_SCOPE_PROCESS(非綁定,預設):多線程多工一個 LWP,由用戶級調度器管理。pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, func, NULL);
pthread_join 回收,否則產生僵屍線程洩漏資源。join。適用於「發射後不管」的任務。pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
陷阱:分離線程若極快結束,可能在 pthread_create 返回前就釋放了線程 ID,導致父線程拿到無效 ID。解決:在子線程開頭 pthread_cond_timedwait 短暫等待,或用互斥鎖同步。
#include <sched.h>
struct sched_param param;
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority = 20; // 視調度策略範圍而定
pthread_attr_setschedparam(&attr, ¶m);
pthread_attr_setschedpolicy(&attr, SCHED_RR); // 或 SCHED_FIFO
pthread_create(&tid, &attr, func, NULL);
需 root 權限或 CAP_SYS_NICE 才能設置實時調度策略。
共享數據帶來競態條件,必須用同步原語保護。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
// 臨界區
pthread_mutex_unlock(&mutex);
pthread_mutexattr_settype 設置為 PTHREAD_MUTEX_RECURSIVE(遞歸鎖)或 PTHREAD_MUTEX_ERRORCHECK(檢錯鎖)。pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
pthread_rwlock_rdlock(&rwlock); // 讀鎖,共享
pthread_rwlock_wrlock(&rwlock); // 寫鎖,排他
pthread_rwlock_unlock(&rwlock);
適合「讀多寫少」場景,並發性能優於互斥鎖。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 等待者
pthread_mutex_lock(&mutex);
while (!condition) pthread_cond_wait(&cond, &mutex);
// 處理...
pthread_mutex_unlock(&mutex);
// 通知者
pthread_mutex_lock(&mutex);
condition = true;
pthread_cond_signal(&cond); // 或 broadcast
pthread_mutex_unlock(&mutex);
關鍵:pthread_cond_wait 原子地釋放鎖並休眠,被喚醒後重新獲取鎖,避免競態。
pthread_spinlock_t spin;
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
pthread_spin_lock(&spin);
// 極短臨界區
pthread_spin_unlock(&spin);
適用於極短臨界區、多核系統,避免線程切換開銷;長時間持有會浪費 CPU。
解決「全域變數在線程間共享,但每線程需要獨立副本」的問題(如 errno、strtok 狀態、資料庫連接句柄)。
pthread_key_create(&key, destructor):創建鍵,可選析構函數(線程退出時自動調用)。pthread_setspecific(key, value):綁定值。pthread_getspecific(key):獲取值。pthread_key_delete(key):銷毀鍵(不調用析構函數)。static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t key;
void make_key() { pthread_key_create(&key, free); }
void *get_connection() {
pthread_once(&once, make_key);
void *conn = pthread_getspecific(key);
if (!conn) {
conn = create_connection();
pthread_setspecific(key, conn);
}
return conn;
}
| 陷阱 | 後果 | 對策 |
|---|---|---|
| 函數內 static 變數 | 多線程共享同一份,數據競態 | 改用 TSD 或傳入緩衝區指針 |
| 返回指向棧/靜態區的指針 | 其他線程修改導致數據錯亂 | 動態分配內存,由調用者釋放;或用 TSD |
| 共享變數未聲明 volatile | 編譯器優化導致讀到舊值 | 加 volatile,並配合鎖/原子操作 |
| 長時間持有鎖 / 嵌套鎖順序不一 | 死鎖、性能下降 | 縮小臨界區、統一加鎖順序、用 pthread_mutex_trylock |
| 分離線程未同步就極快退出 | 父線程獲取無效 tid | 子線程開頭等待條件變數或屏障 |
| 在信號處理器中調用非 async-signal-safe 函數 | 未定義行為 | 僅調用 write、sig_atomic_t 變數操作等安全函數 |
__sync_*、C11 atomic_*)實現無鎖隊列、引用計數等。numactl、pthread_setaffinity_np 綁定線程到特定 CPU 節點,優化記憶體訪問局部性。std::mutex、std::condition_variable、std::future 更安全易用。Linux 多線程編程的核心在於「共享與同步的平衡」。熟練掌握 pthread API、屬性配置、同步原語選型、TSD 機制,並警惕常見陷阱,即可構建高性能、高可靠的併發應用。建議在實戰中優先考慮 C++11/17 標準庫或成熟線程池庫,減少底層錯誤機會。