进阶 #os#thread#concurrency
线程(Thread)与多线程
线程(Thread)是进程内的"轻量级执行流"——同一进程的多个线程共享内存空间,可以并行处理不同任务,就像厨房里多个厨师共用同一套厨具做不同的菜
为什么要线程?
假设你在写一个文本编辑器:
- 用户输入文字
- 同时自动保存
- 同时检查拼写
如果按”进程”的方式:开三个进程,各自干一件事——但共享同一个文档内容就麻烦了(要用进程间通信)。
线程(Thread) 就是同一个进程内的多个”执行流”——它们共享内存空间,但各有各的执行栈和寄存器。
🏫 类比:厨房团队
- 进程 = 整家餐厅(有自己的厨房、仓库、菜单)
- 线程 = 餐厅里的厨师
- 所有厨师(线程)共用同一套厨具和食材(共享内存)
- 但每个厨师有自己的围裙和刀(私有的栈和寄存器)
- 厨师之间可以随时沟通(共享变量),比两个餐厅之间通信容易得多
进程 vs 线程
进程 A 进程 B
┌──────────────────┐ ┌──────────────────┐
│ 代码段 │ │ 代码段 │
│ 数据段 │ │ 数据段 │
│ 堆 │ │ 堆 │
│ ┌──── 线程 1 ──┐ │ │ ┌──── 线程 1 ──┐ │
│ │ 栈 + 寄存器 │ │ │ │ 栈 + 寄存器 │ │
│ └──────────────┘ │ │ └──────────────┘ │
│ ┌──── 线程 2 ──┐ │ │ │
│ │ 栈 + 寄存器 │ │ │ │
│ └──────────────┘ │ │ │
└──────────────────┘ └──────────────────┘
进程之间:隔离(不共享内存)
线程之间:共享代码段、数据段、堆
私有栈和寄存器
| 对比 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立 | 共享(同一进程内) |
| 创建开销 | 高(复制地址空间) | 低(只需分配栈) |
| 切换开销 | 高(TLB 刷新) | 低(不需刷新页表) |
| 通信方式 | IPC(管道、消息队列等) | 直接读共享变量 |
| 隔离性 | 强(一个崩了不影响其他) | 弱(一个线程崩了整个进程挂) |
| 资源占用 | 多 | 少 |
💡 进程是资源分配的最小单位,线程是CPU 调度的最小单位。
线程的实现模型
1. 用户级线程(User-Level Thread, ULT)
线程管理完全在用户空间实现,内核不知道线程的存在。
用户空间 线程库(管理多个线程)
┌──线程1──线程2──线程3──┐
内核空间 单一线程(核心理不到)
优点:切换极快(不需要系统调用),可以在任何 OS 上实现 缺点:一个线程阻塞会导致所有线程阻塞(内核只看到 1 个进程)
2. 内核级线程(Kernel-Level Thread, KLT)
线程的创建、调度、管理全由内核完成。
用户空间 线程1 线程2 线程3
内核空间 ║ ║ ║
内核线程1 内核线程2 内核线程3
优点:一个线程阻塞不影响其他线程,可以充分利用多核 缺点:切换需要系统调用(开销大)
3. 混合模型(Linux 的实现)
Linux 用 clone() 系统调用创建线程——它的灵活之处在于可以精细控制共享什么:
// fork()——完全不共享(创建进程)
clone(CLONE_VM | CLONE_VM | 0);
// pthread_create()——共享内存空间(创建线程)
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...);
💡 在 Linux 中,进程和线程本质上是同一回事——都是
task_struct,区别只在于clone()时传的参数不同(是否共享地址空间)。
多线程编程示例
#include <stdio.h>
#include <pthread.h>
#define NUM 5
void* print_message(void* arg) {
int id = *(int*)arg;
printf("线程 %d:我在运行!\n", id);
return NULL;
}
int main() {
pthread_t threads[NUM];
int ids[NUM];
// 创建 NUM 个线程
for (int i = 0; i < NUM; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, print_message, &ids[i]);
printf("主线程:创建了线程 %d\n", i);
}
// 等待所有线程完成
for (int i = 0; i < NUM; i++) {
pthread_join(threads[i], NULL);
printf("主线程:线程 %d 已结束\n", i);
}
return 0;
}
输出结果(每次运行可能不同——取决于调度顺序):
主线程:创建了线程 0
线程 0:我在运行!
主线程:创建了线程 1
线程 1:我在运行!
线程 2:我在运行!
主线程:创建了线程 2
主线程:创建了线程 3
线程 3:我在运行!
主线程:创建了线程 4
线程 4:我在运行!
主线程:线程 0 已结束
...
⚠️ 多个线程的输出交织在一起——这就是并发!如果线程们同时往一个文件写数据,就需要同步机制(竞争条件与临界区)。
线程的优势
1. 利用多核 CPU
在一个 4 核 CPU 上,4 个线程可以真正并行执行(每个核跑一个)——4 个进程也可以,但线程的切换开销更小。
2. 结构清晰
// 不用线程——一个程序里乱跳
void web_server() {
while (1) {
accept_connection(); // 等连接
handle_request(); // 处理请求(如果慢就卡住)
}
}
// 用线程——每个请求一个线程
void handle_request(int fd) { ... }
void web_server() {
while (1) {
int fd = accept();
pthread_create(&t, NULL, handle_request, &fd);
// 主线程立即回去等下一个连接
}
}
3. I/O 不阻塞
一个线程等网络数据时,其他线程可以继续执行——这在 web 服务器中至关重要。
线程的挑战
| 问题 | 说明 | 后果 |
|---|---|---|
| 竞争条件 | 多个线程同时读写共享变量 | 数据不一致 |
| 死锁 | 线程互相等待对方释放资源 | 程序卡死 |
| 调试困难 | 线程执行顺序不确定 | Bug 难以复现 |
小结
| 概念 | 要点 |
|---|---|
| 线程定义 | 进程内的轻量级执行流,共享地址空间 |
| vs 进程 | 进程隔离、线程共享;进程切换开销大 |
| 用户级线程 | 管理在用户态,切换快但阻塞会影响所有 |
| 内核级线程 | 管理在内核,独立阻塞但切换开销大 |
| Linux 实现 | clone() 统一创建,参数控制共享程度 |
| 多线程价值 | 利用多核、结构清晰、I/O 不阻塞 |
为什么先学这个? 线程是并发的”最小单位”。有了线程,才有了竞争条件与临界区的问题——下一节看看为什么多线程访问共享变量会出问题。