跳转至

线程 — Pthreads 基础与信号困局

这一章讲什么: 进程太重了——fork 复制整个地址空间, 进程间共享数据还得走 IPC。线程是轻量答案:同一个进程 里多条执行流,共享全部内存和文件,只有栈和寄存器是各自的。 本章教 Pthreads 的基本操作(创建/汇合/分离/取消),把第 14 章 的并发服务器改成线程版;最值钱的一节是信号与线程的纠缠—— 它会摧毁你对第 8 章的全部直觉,并逼出「专门线程 sigwait」 这个标准答案。

1. 顶层全景:进程 vs 线程

  进程:资源的分装单位(地址空间、文件、PID)
  线程:调度的执行单位;进程 = 「一个地址空间 + ≥1 条执行流」
      │
  共享(进程级):text/data/堆、全局变量、argv/envp、fd 表、
                umask、CWD、信号处置、定时器、nice、资源上限
  私有(线程级):线程 ID、运行栈+备用栈、栈指针+寄存器、
              信号掩码、errno、调度属性、线程私有数据
      │
  Linux 实现:1:1 模型——每个用户线程 = 一个内核「轻量级进程」
      (clone 出来的 task_struct);task_struct 里资源全是
      指针:共享就指同一个结构,私有就各指一份(图 15-1)

图说:fork 的「复制一切」和线程的「共享一切」是两个极端; 选哪个,看你要不要共享数据以及怕不怕竞争。 历史坐标: 1995 年 Open Group 定 Pthreads(IEEE 1003.1c);2005 年红帽的 NPTL 取代旧实现,性能大升1。编程要 -pthread(不是 -lpthread)2。

与进程原语的对照表(死记这张,API 就通了):

进程 线程 干什么
fork() pthread_create() 造新执行流(传函数指针给新流跑)
exit() pthread_exit() 自己退出,留返回值
waitpid() pthread_join() 等指定对象退出并收状态
getpid() pthread_self() 拿自己的 ID
kill() pthread_cancel() 请求终止对方

2. 线程管理四件套

pthread_create(&tid, attr, start_fn, arg):start_fn 原型 void *fn(void *)——想传多个值?打一个结构体传指针(示范: 数组分段求和,每个线程一个 {tid,id,first,last,array,result} 结构,段界 (t*N)/K 保证段长差 ≤1)3。retval 不能指向 调用方的栈(线程死后栈作废),要全局/静态/堆4。

pthread_join(tid, &ret) = waitpid:等指定线程、收它的退出值 (void** 双重间接)5。四条 undefined 行为警告:两个线程同时 join 同一个、join 非 joinable 的、join 自己、永不 join → zombie 线程(攒多了系统造不出新线程)6。

退出纪律:main 里 return 或任何线程调 exit() → 杀全进程 所有线程;想让 main 先走、孩子们继续跑——main 调 pthread_exit()7。

detached(分离)线程:不需要 join 的就别 join—— pthread_attr_init + setdetachstate(PTHREAD_CREATE_DETACHED) 创建即分离;事后分离 pthread_detach(pthread_self()),不可逆8。

取消(pthread_cancel):只是请求;被请求者可以 setcancelstate(DISABLE) 拒收,或 type 设 ASYNCHRONOUS (立即死)/ DEFERRED(默认:攒到下一个「取消点」函数再死)9。

栈大小:默认栈(常见 8MB,但「通常不超过 8192 字节」的 场景也不少)可能不够;可移植代码显式 pthread_attr_setstacksize;线程里要大缓冲区用堆,别在 栈上开大数组10。

3. 信号与线程:第 8 章直觉的坟墓

三个分离的事实,各自违反直觉11:

  1. 掩码是每线程的(pthread_sigmask, 同 sigprocmask 原型);挂起信号也有每线程一份;
  2. 处置(handler)是全进程一份的——线程 A 装的 handler, 信号落在线程 B 上,也是 A 的那个 handler 跑;
  3. 进程定向信号(kill/键盘 CTRL-C/定时器)发给谁? 内核随便挑一个没挡它的线程——不可预测。

pthread_signal_demo 用三个宏组合的实验确认了全部语义: ps -L 能看到同 PID 下三个 TID;kill -2 打任意一个线程, handler 没注册就全体阵亡(默认动作杀进程)12。

设计推论(原书引 Kerrisk 的标准处方):

多线程程序别做异步信号处理。 正解:① main 在创建任何 线程之前用 sigprocmask 挡掉所有要处理的信号(孩子们 继承这个掩码);② 造一个专门的线程调 sigwait() 同步等 这些信号——信号变成它队列里的事件,没有 handler、没有打断、 没有竞态。第 8 章那个 sigwait 在这才是真正归位。

4. 落点:并发服务器的线程版

第 14 章 upcase 服务器从 fork 改 pthread_create,三个 必须改的点13:

  1. msg 不能传局部变量地址——fork 复制地址空间,每个孩子 有自己的 msg 副本;线程共享内存,主循环里 msg 会被下一条 读覆盖,子线程拿到烂数据。每个请求要 malloc 一份 message, 线程用完自己 free;
  2. 收尸从 SIGCHLD handler 变成 pthread_join——主循环里 每 accept 一个就 join 一个(顺序服务,但不再阻塞监听? 不——join 会阻塞;真并发得 detached + 自清理);
  3. 信号处理按上一节处方重写:全部挡掉 + 专线程 sigwait。

5. 判断/边界/可带走

作者的判断: 「能不用全局共享就不用,传结构体指针」是 贯穿本章的纪律;信号处方直接引 Kerrisk 的权威做法1213。 边界: 调度属性(setschedparam 等)、spinlock、线程 attributes 全家、取消清理栈 pthread_cleanup_push 的细节都没 展开;线程安全的库函数清单留给第 16 章的语境。可带走的: ① 1:1 模型,每线程一个 LWP,task_struct 资源指针化; ② 共享/私有清单一组;③ create/join/detach/cancel 与 fork/wait 对照;④ main 退出用 pthread_exit;⑤ 多线程信号 = 同步专线程 sigwait,别异步。

6. 原文地图

主题 原书章 原文位置
线程历史与 NPTL Chapter 15: Introduction to Threads text/21-ch15-chapter-15-introduction-to-threads.txt:63(搜「Pthreads」) · :69(搜「NPTL」)
共享/私有清单 同上 :140(搜「Shared Resources」) · :180(搜「task_struct」)
与进程原语对照表 同上 :297(搜「Table 15-1」)
create/参数结构体 同上 :346(搜「pthread_create」) · :509(搜「task_data」)
join 与四条警告 同上 :407(搜「pthread_join」) · :432(搜「zombie thread」)
exit 警告 同上 :389(搜「any thread」)
detach 同上 :584(搜「detached thread」) · :617(搜「pthread_detach」)
cancel 语义 同上 :676(搜「request」) · :692(搜「pthread_setcancelstate」)
栈大小 同上 :716(搜「pthread_attr_setstacksize」)
信号三事实 同上 :820(搜「same prototype」) · :824(搜「process wide」) · :835(搜「any one of the threads」)
sigwait 处方 同上 :970(搜「Kerrisk」)
服务器线程化三坑 同上 :1012(搜「corrupted data」)

  1. 出处:「Chapter 15: Introduction to Threads」第 55–71 段(text/21-ch15-chapter-15-introduction-to-threads.txt:63,搜「Pthreads」) · :69(搜「NPTL」)。 ↩

  2. 出处:「Chapter 15: Introduction to Threads」第 311–313 段(text/21-ch15-chapter-15-introduction-to-threads.txt:312,搜「-pthread」)。 ↩

  3. 出处:「Chapter 15: Introduction to Threads」第 342–361 段(text/21-ch15-chapter-15-introduction-to-threads.txt:346,搜「pthread_create」) · :509(搜「task_data」)。 ↩

  4. 出处:「Chapter 15: Introduction to Threads」第 377–380 段(text/21-ch15-chapter-15-introduction-to-threads.txt:377,搜「should not be located」)。 ↩

  5. 出处:「Chapter 15: Introduction to Threads」第 399–423 段(text/21-ch15-chapter-15-introduction-to-threads.txt:407,搜「pthread_join」)。 ↩

  6. 出处:「Chapter 15: Introduction to Threads」第 424–435 段(text/21-ch15-chapter-15-introduction-to-threads.txt:432,搜「zombie thread」)。 ↩

  7. 出处:「Chapter 15: Introduction to Threads」第 389–397 段(text/21-ch15-chapter-15-introduction-to-threads.txt:389,搜「any thread」)。 ↩

  8. 出处:「Chapter 15: Introduction to Threads」第 573–625 段(text/21-ch15-chapter-15-introduction-to-threads.txt:584,搜「detached thread」) · :617(搜「pthread_detach」)。 ↩

  9. 出处:「Chapter 15: Introduction to Threads」第 668–705 段(text/21-ch15-chapter-15-introduction-to-threads.txt:676,搜「request」) · :692(搜「pthread_setcancelstate」)。 ↩

  10. 出处:「Chapter 15: Introduction to Threads」第 707–761 段(text/21-ch15-chapter-15-introduction-to-threads.txt:716,搜「pthread_attr_setstacksize」)。 ↩

  11. 出处:「Chapter 15: Introduction to Threads」第 817–843 段(text/21-ch15-chapter-15-introduction-to-threads.txt:820,搜「same prototype」) · :824(搜「process wide」) · :835(搜「any one of the threads」)。 ↩

  12. 出处:「Chapter 15: Introduction to Threads」第 844–976 段(text/21-ch15-chapter-15-introduction-to-threads.txt:956,搜「ps -L」) · :970(搜「Kerrisk」)。 ↩↩

  13. 出处:「Chapter 15: Introduction to Threads」第 978–1050 段(text/21-ch15-chapter-15-introduction-to-threads.txt:1012,搜「corrupted data」)。 ↩↩