第 29 章 线程:介绍 (Threads: Introduction)
核心结论
-
线程共享进程的全局内存(text/data/bss/heap)+ 大部分进程属性:每个线程有独立栈、线程 ID、信号屏蔽字、errno、线程特定数据、浮点环境等;线程栈都在同一虚拟地址空间内,可能交错。
-
线程 vs 进程的核心权衡:线程创建更快(~10x)、共享数据只需访问全局变量;但所有线程共享地址空间——单线程 bug 可损坏所有线程;需 thread-safe 编程;多线程程序都跑同一程序。
-
pthread_create() 创建新线程:新线程从
start(arg)开始执行;pthread_t buffer 在返回前不保证初始化;返回值用 errExitEN() 处理(pthread 函数返回 0/正错误码,非 -1/errno)。 -
pthread_exit()/return/cancel/exit 终止线程:主线程 return 或任何线程 exit → 整个进程终止;pthread_exit 只终止当前线程;retval 不能指向线程栈(线程终止后栈可能被新线程复用)。
-
pthread_join() 等价于 waitpid() 但只针对特定 TID:无「等任意线程」、无 WNOHANG;线程是 peer 关系——任何线程可 join 任何线程;join 后线程状态被回收(不 join 未 detach 线程 → 线程僵尸,浪费资源)。
-
pthread_detach() 或 pthread_attr_setdetachstate 标记线程自动清理:detach 后不能再 join;用于不关心返回值的 worker 线程。
|
本章主旨
本章是「线程」四章的导论——理解 POSIX 线程的基本模型与生命周期。读者应掌握:线程与进程的差异(共享 vs 私有属性)、Pthreads 数据类型(pthread_t 等不透明类型)、线程创建(pthread_create + start 函数)、线程终止(4 种方式)、线程 ID(pthread_self/pthread_equal)、join/detach 模型、线程属性(pthread_attr_t 含 detachstate/栈/调度)、线程 vs 进程的设计取舍。理解 pthread_join 的「只针对特定 TID」设计是写「线程池 + 工作队列」的关键;理解共享与私有属性的清晰划分是写正确同步代码的前提。 |
一、核心概念
本章围绕 6 个核心概念展开:从线程模型入手,到 Pthreads API 背景、线程创建、线程终止、线程 ID 与 join/detach,最后是线程 vs 进程的设计取舍。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
线程共享/私有属性 |
线程共享:PID/PPID/PGID/SID/凭证/fd/信号 disposition/锁/cwd/root/umask/timers/资源限制/CPU 时间/nice。线程私有:TID/信号屏蔽字/线程特定数据/alternate signal stack/errno/浮点环境/调度策略/CPU 亲和性/capabilities/栈。 |
§29.1;理解「共享什么」决定同步需求,「私有什么」决定 thread-local 状态;图 29-1 展示四线程进程内存布局。 |
Pthreads 数据类型与 errno 模型 |
pthread_t/mutex/cond/key/attr 是不透明类型——不能用 == 比较;每个线程有独立 errno(用宏实现为函数调用返回 modifiable lvalue);Pthreads 函数返回 0 或正错误码(非 -1/errno)。 |
§29.2;编译加 |
pthread_create() |
创建新线程,从 start(arg) 开始执行;pthread_t buffer 不保证在 pthread_create 返回前初始化——子线程若需自己的 ID 用 pthread_self();attr=NULL 用默认属性;arg 不能是线程栈上对象。 |
§29.3;start 返回值不能与 PTHREAD_CANCELED 同值(线程取消时);retval 不能指向线程栈。 |
线程终止 4 种方式 |
start return / pthread_exit(retval) / pthread_cancel / exit()(或 main return)——后者终止所有线程;retval 不能在线程栈上(线程终止后栈可能被新线程复用)。 |
§29.4;主线程 pthread_exit 不终止进程(区别于 main return);非 detach 线程必须 join 否则成为「线程僵尸」。 |
pthread_join() / pthread_detach() |
join 类似 waitpid:等特定 TID 终止 + 回收状态;无「等任意」、无 WNOHANG(条件变量可模拟);detach 标记线程自动清理——不能再 join;不 detach 不 join → 线程僵尸。 |
§29.5-§29.7;线程是 peer 关系——任何线程可 join 任何线程(包括「祖父线程 join 孙线程」);detach 控制终止后行为,不控制终止方式。 |
线程 vs 进程的设计取舍 |
线程优势:数据共享简单、创建快 ~10x、context switch 可能更快;线程劣势:单线程 bug 损所有线程、需 thread-safe、所有线程跑同一程序、虚拟地址空间共享。 |
§29.9;多进程 + 共享内存(mmap/SysV/POSIX)vs 多线程;信号在多线程中难处理(§33.2);一般原则:CPU bound + 数据共享 → 线程;隔离 + 不同程序 → 进程。 |
二、详细笔记
29.1 线程模型与共享/私有属性
What:线程是进程内的执行流;同一进程的所有线程共享 text/data/bss/heap + 大部分进程属性;每个线程有独立栈、线程 ID、errno 等。
Why:理解「什么共享、什么私有」是写多线程程序的根——共享决定同步需求,私有决定线程本地状态。
How:
共享属性(§29.1):
-
进程 ID、PPID、PGID、SID、控制终端。
-
凭证:real/effective/saved UID/GID、supplementary GIDs。
-
打开文件描述符表(独立 fd table 但共享打开文件描述 + 偏移 + 状态)。
-
fcntl record locks。
-
信号 disposition。
-
文件系统:umask/cwd/root。
-
计时器:interval timer(setitimer)、POSIX timer(timer_create)、SysV semaphore undo。
-
资源限制、CPU 时间(times)、资源使用(getrusage)、nice 值。
私有属性(§29.1):
-
线程 ID(pthread_self)、进程内的 TID 唯一性。
-
信号屏蔽字(sigprocmask)。
-
线程特定数据(pthread_key_create,详见第 31 章)。
-
备用信号栈(sigaltstack)。
-
errno(线程特定宏实现)。
-
浮点环境(fenv)。
-
实时调度策略与优先级(SCHED_FIFO/RR/DEADLINE)。
-
CPU 亲和性(sched_setaffinity)。
-
capabilities(per-thread)。
-
栈(局部变量 + 函数调用链)。
线程栈的关键陷阱(§29.1):所有线程栈在同一虚拟地址空间——可能交错;栈帧随函数返回被复用;线程终止后栈可能被新线程复用——这是「retval 不能指向线程栈」的原因。
When:设计多线程程序时——明确「共享数据需 mutex/cond」「私有数据用 pthread_key_create 或 thread-local 变量」。
Example:
// 摘自《The Linux Programming Interface》第 29 章 — 共享 vs 私有
/* 共享:全局变量 */
int shared_counter = 0;
/* 私有:线程特定 errno */
errno = EINTR; /* 只影响当前线程 */
/* 私有:线程栈上的局部变量 */
void *worker(void *arg) {
int local = 42; /* 其他线程看不到 */
return NULL;
}
29.2 Pthreads API 背景
What:POSIX.1c (1995) 标准化的 POSIX 线程 API——SUSv3 包含;定义一组不透明数据类型;errno 改为线程特定;Pthreads 函数返回 0 或正错误码。
Why:理解 Pthreads API 的「反传统 UNIX」约定(不返回 -1/errno)是写正确 Pthreads 代码的前提;编译选项 -pthread 是必须的。
How:
不透明数据类型(§29.2,表 29-1):
-
pthread_t:线程 ID(Linux NPTL 是unsigned long,实际是 cast 的指针)。 -
pthread_mutex_t/pthread_mutexattr_t:互斥量与属性。 -
pthread_cond_t/pthread_condattr_t:条件变量与属性。 -
pthread_key_t:线程特定数据 key。 -
pthread_once_t:一次性初始化控制。 -
pthread_attr_t:线程属性对象。
关键约束(§29.2):
-
不能用
==比较 pthread_t——必须用pthread_equal(t1, t2)。 -
不能用 printf("%ld", (long) tid) 打印——非可移植;SUSv3 允许 pthread_t 是结构体。
-
errno 改为线程特定——
<errno.h>必须包含;extern int errno 在 SUSv3 中不允许。
Pthreads 函数返回约定(§29.2):
-
0 → 成功。
-
正值 → 错误码(与 errno 值同集合,但直接返回,不通过 errno)。
-
示例:
s = pthread_create(…);if (s != 0) errExitEN(s, "pthread_create");
编译选项(§29.2):
-
Linux:
cc -pthread(定义 _REENTRANT + 链接 libpthread)。 -
Solaris/HP-UX:
cc -mt。 -
Tru64:
cc -pthread(同 Linux)。
When:任何使用 Pthreads 的程序必须包含 <pthread.h>、编译加 -pthread;用 errExitEN 包装所有 pthread_* 调用。
Example:
// 摘自《The Linux Programming Interface》第 29 章 — Pthreads 错误处理模式
#include <pthread.h>
int s;
pthread_t t;
s = pthread_create(&t, NULL, func, &arg);
if (s != 0) errExitEN(s, "pthread_create");
/* 不能再用 errno 检查 — 错误码在 s */
29.3 pthread_create() 创建线程
What:pthread_create(&t, attr, start, arg) 创建新线程;新线程从 start(arg) 开始执行;调用者继续下一语句。
Why:多线程程序入口;理解 start/arg 语义(单一指针参数,复合参数需用结构体)是写线程函数的关键。
How:
签名(§29.3):
// 摘自《The Linux Programming Interface》第 29 章
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start)(void *), void *arg);
要点(§29.3):
-
start 函数签名
void ()(void *);arg 是 void *——可传递任意类型指针;多参数用结构体指针。 -
pthread_t buffer 在 pthread_create 返回前不一定初始化——新线程可能在 pthread_create 返回前就开始运行;新线程若需自己的 ID 用
pthread_self()。 -
attr=NULL → 默认属性(joinable + 默认栈 + 默认调度)。
-
start 返回值是
void *——同样可传递任意指针;join 时通过 pthread_join 的 retval 接收。 -
不要让 start 返回与 PTHREAD_CANCELED 相同的整数(cast 后的)——join 时会误判为被取消。
-
pthread_create 返回后无线程调度保证——多线程可能被同时调度到不同 CPU(多核系统);需同步见第 30 章。
When:任何需要并发执行的场景——网络服务器 worker、并行计算、异步 I/O 处理器;优先考虑用 pthread_attr_setdetachstate 创建时直接 detach 而非 join。
Example:
// 摘自《The Linux Programming Interface》第 29 章 — Listing 29-1
// 摘自 threads/simple_thread.c
static void *threadFunc(void *arg) {
char *s = (char *) arg;
printf("%s", s);
return (void *) strlen(s);
}
int main(int argc, char *argv[]) {
pthread_t t1;
void *res;
int s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");
if (s != 0) errExitEN(s, "pthread_create");
printf("Message from main()\n");
s = pthread_join(t1, &res);
if (s != 0) errExitEN(s, "pthread_join");
printf("Thread returned %ld\n", (long) res);
exit(EXIT_SUCCESS);
}
29.4 线程终止
What:线程终止 4 种方式——start return / pthread_exit / pthread_cancel / exit()(或 main return);后两者终止整个进程。
Why:理解线程终止语义是控制程序生命周期的关键——main return 终止所有线程 ≠ pthread_exit 仅终止主线程。
How:
pthread_exit()(§29.4):
// 摘自《The Linux Programming Interface》第 29 章
#include <pthread.h>
void pthread_exit(void *retval);
关键约束:
-
retval 不能指向线程栈上的对象——线程终止后栈帧被新线程复用。
-
pthread_exit 等价于 start 函数 return,但可从任意被 start 调用的函数中调用。
-
主线程调 pthread_exit → 进程继续运行直到所有线程终止或 exit。
-
主线程调 exit() 或 main return → 进程立即终止,未结束线程被强制 kill。
-
pthread_cancel 在第 32 章详细讨论。
When:线程函数执行完毕 → return;线程需提前退出但保留其他线程 → pthread_exit;线程退出时需清理资源 → 注册 pthread_cleanup_push handler(详见第 32 章)。
Example:
// 摘自《The Linux Programming Interface》第 29 章 — 主线程退出模式
void *worker(void *arg) {
/* 长期运行的工作 */
while (!done) {
/* ... */
}
pthread_exit((void *) 0); /* 不调用 exit() */
}
int main() {
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
sleep(5);
done = 1;
pthread_join(t, NULL);
/* 主线程不调 exit,worker 自然终止 */
return 0;
}
29.5-29.6 线程 ID 与 pthread_join()
What:每个线程有 pthread_t ID(pthread_self 返回);pthread_join 等价 waitpid 但只等特定 TID——无「等任意」、无非阻塞。
Why:线程是 peer 关系——任何线程可 join 任何线程;理解 join 模型的限制(不能等任意线程)是用条件变量实现线程池的前提。
How:
线程 ID API(§29.5):
// 摘自《The Linux Programming Interface》第 29 章
pthread_t pthread_self(void);
int pthread_equal(pthread_t t1, pthread_t t2); /* 非 0 = 相等 */
关键点:
-
pthread_t 是不透明——必须用 pthread_equal 比较。
-
Linux NPTL 中 pthread_t 是 cast 的指针——可移植代码不要解引用。
-
pthread_t ID 可被重用——join 后或 detach 线程终止后 ID 可能被新线程复用。
-
POSIX pthread_t ≠ Linux gettid() 系统调用返回值——前者是用户态,后者是内核线程 ID。
pthread_join()(§29.6):
// 摘自《The Linux Programming Interface》第 29 章
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
要点(§29.6):
-
等特定 thread 终止;已终止则立即返回。
-
retval 非 NULL → 接收终止线程的返回值(return 或 pthread_exit 指定的)。
-
无「等任意线程」——SUSv3 故意限制;否则库函数无法安全 join 私有线程(可能 join 错了 ID)。
-
无 WNOHANG 非阻塞——用条件变量 + mutex 实现(§30.2.4)。
-
线程是 peer——A 创建 B 创建 C,A 可 join C(区别于进程父子层级)。
-
不可对已 join 的 TID 再次 join——结果未定义(可能错误 join 到重用 ID 的新线程)。
-
不 join 未 detach 线程 → 线程僵尸;积累到上限后 pthread_create 失败 EAGAIN。
When:关心线程返回值 → join;不关心返回值且不希望线程僵尸 → detach;线程池 + 工作者 → 用条件变量模拟「join any」。
Example:
// 摘自《The Linux Programming Interface》第 29 章 — 祖父线程 join 孙线程
pthread_t grandchild;
void *grandchild_fn(void *arg) {
return (void *) 42;
}
void *child_fn(void *arg) {
pthread_t gc;
pthread_create(&gc, NULL, grandchild_fn, NULL);
pthread_exit(NULL); /* child 退出,但孙线程继续 */
}
int main() {
pthread_t c, gc;
pthread_create(&c, NULL, child_fn, NULL);
pthread_join(c, NULL); /* 等 child */
/* 祖父线程直接 join 孙线程 */
pthread_join(gc, NULL);
return 0;
}
29.7-29.8 detach 与线程属性
What:pthread_detach 标记线程自动清理(不可再 join);pthread_attr_t 含 detachstate/栈地址大小/调度策略与优先级等属性。
Why:用 detach 避免线程僵尸 + 减少 join 样板;线程属性用于在创建时配置栈大小、调度策略等。
How:
pthread_detach()(§29.7):
// 摘自《The Linux Programming Interface》第 29 章
#include <pthread.h>
int pthread_detach(pthread_t thread);
要点:
-
detach 后线程终止时自动清理,不能再 join。
-
线程可自 detach:
pthread_detach(pthread_self())。 -
detach 不影响线程运行——只是终止后的清理方式。
-
exit() / main return 仍终止所有线程(detach 也不免疫)。
线程属性(§29.8):
// 摘自《The Linux Programming Interface》第 29 章 — Listing 29-2
// 摘自 threads/detached_attrib.c
pthread_attr_t attr;
int s = pthread_attr_init(&attr);
s = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t thr;
s = pthread_create(&thr, &attr, threadFunc, (void *) 1);
s = pthread_attr_destroy(&attr); /* 销毁属性对象 */
常用属性:
-
detachstate:PTHREAD_CREATE_JOINABLE(默认)/PTHREAD_CREATE_DETACHED。 -
stackaddr+stacksize:自定义线程栈(默认栈大小通常 8 MB;可缩小节省虚拟地址空间)。 -
schedpolicy+schedparam:调度策略与优先级(SCHED_OTHER/FIFO/RR/DEADLINE)。 -
inheritsched:PTHREAD_INHERIT_SCHED(继承创建者)/PTHREAD_EXPLICIT_SCHED(用 attr)。 -
scope:争用范围(PTHREAD_SCOPE_SYSTEM/PTHREAD_SCOPE_PROCESS,Linux 仅前者)。 -
guardsize:栈溢出保护页大小(默认 PAGE_SIZE)。
When:worker 线程用 detach 避免 join 样板;大量线程(数千)需定制栈大小;实时线程用 schedpolicy 设置 FIFO/RR。
Example:
// 摘自《The Linux Programming Interface》第 29 章 — 创建时直接 detach
// 摘自 threads/detached_attrib.c
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t thr;
pthread_create(&thr, &attr, threadFunc, (void *) 1);
pthread_attr_destroy(&attr);
/* 不能 pthread_join(thr, NULL) — 已 detach */
29.9 线程 vs 进程
What:设计多任务程序时需在「线程共享地址空间」与「进程独立地址空间」之间选择。
Why:理解两种模型的取舍是系统架构设计的关键——CPU-bound 并行、I/O 密集、隔离需求等都影响选择。
How:
线程优势(§29.9):
-
数据共享简单——直接读写全局变量。
-
创建快 ~10x——无需复制页表。
-
Context switch 可能更快——共享地址空间,TLB 命中率高。
线程劣势(§29.9):
-
需 thread-safe 编程——所有库函数都需考虑并发;详见第 31 章。
-
单线程 bug 可损坏所有线程——空指针解引用 → 整个进程 crash。
-
虚拟地址空间共享——大量线程受限于进程 VA 空间(典型 x86-32 3 GB)。
-
所有线程跑同一程序——不能 fork 不同二进制。
-
信号处理复杂——见第 33 章。
-
共享某些信息(fd、cwd 等)可能是优点也可能是缺点——取决于应用。
一般原则(§29.9):
-
数据共享密集 + 同程序 → 线程。
-
隔离 + 不同程序 + 安全 → 进程。
-
I/O 密集 + 数据共享 → 线程(但第 63 章的 epoll/io_uring 可能是更好选择)。
-
高吞吐网络服务器 → 线程池或 epoll 单线程(看具体负载)。
When:写新并发程序时——先评估「需要共享多少数据」「是否需要不同程序」「隔离要求」;CPU-bound + 数据共享 → 线程池;I/O-bound + 高并发 → 优先 epoll/io_uring。
Example:
// 摘自《The Linux Programming Interface》第 29 章 — 线程 vs 进程选择
/* 数据共享密集场景:线程 */
typedef struct { int request_count; double total; } Stats;
static Stats shared_stats; /* 多线程直接读写 — 需 mutex */
/* 不同程序/强隔离:进程 */
pid_t pid = fork();
if (pid == 0) {
/* 子进程执行不同二进制 — exec() */
execl("/bin/worker", "worker", (char *) NULL);
}
三、关键图表
|
线程共享/私有属性对照表
|
|
Pthreads 函数返回约定
|
四、思维导图
mindmap
root((第 29 章 线程介绍))
线程模型
共享 text data bss heap
共享 fd 凭证 cwd 计时器
私有栈 TID errno
私有信号屏蔽字 sched
私有 capabilities
Pthreads API
POSIX 1c 1995 标准
pthread t 不透明
pthread equal 比较
errno 线程特定
返回 0 或正错误码
编译 -pthread
pthread create
start arg 启动
attr 默认 joinable
pthread t 不保证先初始化
pthread self 取自己 ID
start 返回不能匹配 CANCELED
线程终止
start return
pthread exit
pthread cancel
exit 终止所有线程
retval 不能在栈上
pthread join
等特定 TID
无 join any
无 WNOHANG
线程是 peer
不 join 未 detach 线程僵尸
detach 与属性
pthread detach
pthread attr t
detachstate joinable detached
stackaddr stacksize
schedpolicy schedparam
自定义栈节省 VA
线程 vs 进程
数据共享简单
创建快 10x
thread safe 必需
单 bug 损所有线程
VA 空间共享
多进程强隔离
五、重点与易错点
-
线程共享进程的全局内存 + 大部分进程属性——共享:PID/PPID/PGID/SID/凭证/fd/信号 disposition/cwd/root/umask/计时器/资源限制/nice;私有:TID/信号屏蔽字/线程特定数据/errno/浮点环境/调度/CPU 亲和性/capabilities/栈。
-
pthread_t 是不透明类型——不能用 == 比较,必须用 pthread_equal;Linux NPTL 中是 cast 指针,可移植代码不要解引用。
-
errno 在多线程中是宏展开为函数调用——每个线程有独立 errno;
<errno.h>必须包含;extern int errno 在 SUSv3 中不允许。 -
Pthreads 函数返回 0 或正错误码——不是 -1/errno;用 errExitEN(s, "func") 而非 errExit("func") 包装。
-
编译加
-pthread——同时定义 _REENTRANT + 链接 libpthread;其他平台用-mt(Solaris/HP-UX)。 -
pthread_create 返回前 pthread_t buffer 不一定初始化——新线程可能在 pthread_create 返回前就运行;新线程需自己的 ID 用 pthread_self()。
-
线程终止的 retval 不能指向线程栈——线程终止后栈帧可能被新线程复用;同样规则适用 start 函数的 return 值。
-
pthread_exit 不等于 exit——主线程 pthread_exit 仅终止主线程,其他线程继续;主线程 return / exit 终止整个进程。
-
pthread_join 无「等任意」、无 WNOHANG——只等特定 TID;非阻塞 join 用条件变量 + mutex 模拟(§30.2.4)。
-
线程是 peer 关系——任何线程可 join 任何线程;A 创建 B 创建 C,A 可 join C;区别于进程父子层级。
-
不 join 未 detach 线程 → 线程僵尸——积累到上限后 pthread_create 失败 EAGAIN;同进程僵尸。
-
detach 不影响线程运行——只控制终止后的清理方式;exit() / main return 仍强制终止所有线程(detach 不免疫)。
-
start 返回值不能与 PTHREAD_CANCELED 相同——否则 join 时误判为被取消;NPTL 中 PTHREAD_CANCELED 是 ((void *) -1)。
-
pthread_join 已 join 的 TID 结果未定义——可能 join 到重用 ID 的新线程;务必用专用变量保存 TID。
-
线程栈都在同一 VA 空间——可能交错;栈地址不能用绝对值;调试时需注意多栈交错导致的复杂栈回溯。
-
线程创建比进程快约 10 倍——基于 clone(CLONE_VM|…);无需复制页表 + 无 COW 标记。
-
线程共享 fd 偏移与状态标志——多线程同时 read/write 同一 fd → 竞争条件;线程间同步 fd 访问需 mutex 或独立 fd 副本(dup)。
-
大量线程受限于 VA 空间——x86-32 典型 3 GB VA;每线程默认栈 8 MB + 线程特定数据;可定制 stacksize 减小占用。
-
跨章衔接:第 30 章线程同步(mutex/cond)→ 解决共享数据竞争;第 31 章 thread-safety 与 per-thread storage → 解决 errno/可重入;第 32 章线程取消 → pthread_cancel + cleanup handlers;第 33 章线程细节 → 信号交互、LinuxThreads vs NPTL;第 35 章调度 → 实时线程 SCHED_FIFO/RR;第 60 章 → 服务器设计模式(线程 vs 进程)。
-