进程与线程
"进程是资源分配的单位,线程是 CPU 调度的单位"——这句经典论断几乎出现在每场操作系统面试里。本文讲清进程与线程的区别、进程的状态变化,以及语言层面的对应实现。
什么是进程
进程是正在运行的程序实例:程序是静态的代码与数据,进程是动态的执行过程。每个进程拥有独立的地址空间、文件描述符、寄存器现场等资源,进程之间默认相互隔离。
┌─────────────────────────┐
│ 进程 A:独立的地址空间 │ 进程 B 看不到 A 的内存,
│ 代码 | 数据 | 堆 | 栈 │ 互不干扰,一个崩溃不影响另一个
└─────────────────────────┘
Linux 下用 fork() 创建子进程:子进程是父进程的一份"拷贝",之后各自独立运行。
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // 父进程返回子进程 pid,子进程返回 0
if (pid == 0) {
printf("我是子进程\n");
} else {
printf("我是父进程,子进程 pid=%d\n", pid);
}
return 0;
}
// 输出:我是父进程,子进程 pid=1234
// 我是子进程(两行顺序不定)
什么是线程
线程是进程内部的"一条执行流"。一个进程可以包含多个线程,它们共享该进程的地址空间与资源,所以创建和切换的开销远小于进程,但也因此引入了同步问题。
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源 | 独立地址空间 | 共享进程资源 |
| 创建/切换成本 | 高 | 低 |
| 相互影响 | 隔离,崩溃不连坐 | 共享数据需加锁 |
| 通信方式 | 需 IPC 机制 | 直接读写共享变量 |
| 拥有者 | 拥有资源 | 拥有 CPU 执行权(栈、寄存器) |
进程的三种基本状态
进程在生命周期中不停地在状态间切换:
新建 → 就绪 ⇄ 运行 → 阻塞
↑ │
└───────────┘(等待的事件完成,回到就绪)
- 就绪:万事俱备,只等 CPU;
- 运行:正在 CPU 上执行;
- 阻塞:等待 I/O 或某事件(如等待键盘输入、等待锁)。
面试高频点:就绪 → 运行叫调度;运行 → 就绪叫抢占(时间片耗尽或被更高优先级打断);运行 → 阻塞是进程自己发起 I/O。
语言中的线程与协程
- Java:
new Thread(() -> {}).start(),JVM 线程通常映射到操作系统线程。 - Python:
threading模块,但受 GIL 限制,CPU 密集任务应改用多进程。 - Go:goroutine 是用户态协程,由 Go 运行时调度到少量 OS 线程上,可轻松开上万个。
// 以下片段需放入 main 函数中运行:goroutine 基本用法
go func() { fmt.Println("协程 1") }()
go func() { fmt.Println("协程 2") }()
time.Sleep(time.Millisecond) // 简单等待协程执行完毕
// 输出:协程 1 与协程 2,顺序不定
小结:进程负责"圈地"(资源隔离),线程负责"干活"(CPU 执行)。要隔离用进程,要高并发低开销用线程或协程;而"共享内存带来的同步问题",正是后续进程间通信与锁相关内容的引子。