核心 02-进程线程与调度 预计 20 分钟 kp-008

线程:并发执行单元与线程模型

线程是 CPU 调度的基本单位:同一进程内的多个线程共享地址空间与资源,各自拥有独立的栈、寄存器上下文与调度状态。

学习状态:

一句话定义

线程是 CPU 调度的基本单位:同一进程内的多个线程共享地址空间与资源,各自拥有独立的栈、寄存器上下文与调度状态。

为什么重要

进程解决了隔离,线程解决了"同一程序内并发"的效率问题:共享内存使数据交换零拷贝,切换成本低于进程切换(kp-006)。但共享也是一切并发 bug 的根源——从本节起,竞态、锁、死锁(kp-011~kp-015)的故事都发生在多个线程同时读改同一内存的场景里。

前置知识

kp-005(进程与 PCB);kp-006(切换成本差异的原因)。

核心概念

  • 线程私有:栈、寄存器组、程序计数器、errno 等线程局部数据。
  • 线程共享:地址空间(代码、全局变量、堆)、打开文件表、信号处理、子进程。
  • KLT(内核级线程):内核直接调度,可利用多核,切换需陷入内核。
  • ULT(用户级线程)/ 协程:用户态库自行调度,切换极廉价,但内核看不到,阻塞系统调用会拖累全部线程。
  • M:N 模型:M 个用户线程映射到 N 个内核线程,试图兼得,复杂度高(Go 的 goroutine 调度器是其现代代表)。
  • TCB:线程控制块,内核或库中记录线程状态的机构。

原理与机制

单线程与多线程进程的内存布局对比:

单线程进程                 多线程进程(4 线程)
+-----------+             +-----------+
|  栈        |             | 栈4 栈3   |
|  (向下长)  |             | 栈2 栈1   |   每线程一个独立栈
+-----------+             +-----------+
|  空闲      |             |  空闲      |
+-----------+             +-----------+
|  堆        |             |  堆        |   共享:分配器需并发安全
|  全局数据   |             |  全局数据   |   共享:需要同步保护
|  代码      |             |  代码      |   共享(只读)
+-----------+             +-----------+

pthread 的最小骨架(C):

#include <pthread.h>
void* worker(void* arg) { return NULL; }
int main(void) {
    pthread_t tids[4];
    for (int i = 0; i < 4; i++)
        pthread_create(&tids[i], NULL, worker, NULL);
    for (int i = 0; i < 4; i++)
        pthread_join(tids[i], NULL);   /* 等待结束,等价于线程版 wait */
    return 0;
}

编译 gcc -pthread demo.c;线程默认栈 8MB(虚拟地址空间预留,物理内存按需分配,呼应 kp-019)。

直观类比

进程像各自开火做饭的分租公寓(独立厨房互不影响但串门麻烦),线程像一家人共用一个厨房(共享冰箱省事,但两个人同时抢一个锅就会出事故——竞态条件,见 kp-011)。

实例或案例

ps -Lf <pid>          # 列出进程的所有线程(LWP 号即内核线程 ID)
ulimit -s             # 查看线程默认栈上限
top -H -p <pid>       # 按线程维度观察 CPU 占用(Java/JVM 排障常用)

典型案例:Web 服务器"每连接一线程"模型在线程数过万时切换与内存开销失控,引出 kp-028 的事件驱动与 io_uring;"线程池为什么设成核数到 2 倍核数"是 kp-010 调度与 Amdahl 定律(kp-032)的联合应用题。

常见误区

  • 线程数越多吞吐越高:超过物理核数后收益递减,切换与锁争用反而拉低性能。
  • 把局部变量当线程安全:局部变量在私有栈上天然隔离,但通过指针泄漏出去就不再安全。
  • 认为 ULT 已被淘汰:协程在 I/O 密集服务中复兴(goroutine、async/await),其代价与 ULT 时代一致——阻塞调用与调度公平性。

与其他知识点的关系

kp-011 起的整个并发模块都以多线程为背景;kp-029 容器以 cgroup 限额的就是一组进程/线程;kp-032 用 Amdahl 定律量化"多线程提速上限"。

延伸阅读

《现代操作系统》第 2 章线程节;Ousterhout 1996 年"Why Threads Are a Bad Idea"作为反方视角。

自测题

  1. 线程间共享哪些资源、私有哪些资源?

答:共享地址空间(堆、全局数据、代码)、打开文件表与信号处理;私有栈、寄存器上下文与程序计数器。

  1. KLT 与 ULT 的核心权衡是什么?

答:KLT 由内核调度、能用多核且单线程阻塞不拖累他人,但切换要陷入内核;ULT 切换廉价且调度灵活,但无法利用多核、一个线程阻塞系统调用会让整组线程停摆。

  1. 为什么多线程的堆分配器必须特殊设计?

答:多个线程并发 malloc/free 操作同一堆元数据,若不加锁或分区(如 tcmalloc 的线程缓存)就会产生竞态条件。

标签:#线程 #pthread #ULT #KLT #并发