进程线程概念、状态转换与调度
| 维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 地址空间 | 独立的地址空间 | 共享进程的地址空间 |
| 资源开销 | 创建/切换开销大(需要分配内存、文件描述符等) | 创建/切换开销小(只需分配栈) |
| 通信方式 | IPC(管道、消息队列、共享内存、Socket) | 直接读写共享变量(需要同步) |
| 独立性 | 高度独立,一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
| 内存结构 | 独立的代码段、数据段、堆、栈 | 共享代码段、数据段、堆,独立栈 |
| 状态 | 说明 | Linux 标识 |
|---|---|---|
| 新建(New) | 进程正在被创建,PCB 已分配但未完成初始化 | - |
| 就绪(Ready) | 等待 CPU 分配,具备运行条件 | R |
| 运行(Running) | 正在 CPU 上执行指令 | R |
| 阻塞(Blocked/Waiting) | 等待某个事件(I/O 完成、信号量、锁) | S/D |
| 终止(Terminated) | 进程执行完毕或被终止,等待回收资源 | Z |
fork()
↓
┌─────┐
│ 新建 │
└──┬──┘
↓ 就绪
┌─────┐ 调度 ┌─────┐
┌→ │ 就绪 │ ──────────→ │ 运行 │
│ └─────┘ └──┬──┘
│ ↑ │
│ │ 时间片用完/被抢占 │
│ └─────────────────────┘
│ ↑
│ │ I/O 完成/事件发生
│ ┌─────┐
└─ │ 阻塞 │ ←─── I/O 请求/等待事件
└─────┘
↑
│
┌─────┐
│ 运行 │ ──→ exit() ──→ 终止
└─────┘| 方式 | 特点 | 性能 | 适用场景 |
|---|---|---|---|
| 管道(Pipe) | 半双工,父子进程 | 中 | 简单的单向数据流 |
| 命名管道(FIFO) | 半双工,无亲缘关系 | 中 | 不相关进程的单向通信 |
| 消息队列 | 异步,消息有类型 | 中 | 解耦的异步消息传递 |
| 共享内存 | 最快,需要同步 | 高 | 大量数据交换 |
| 信号量 | 用于同步 | 高 | 进程/线程同步 |
| 信号(Signal) | 异步通知 | 高 | 事件通知、进程控制 |
| 套接字(Socket) | 支持网络通信 | 低 | 跨主机通信、C/S 架构 |
特点:半双工(单向通信),只能用于有亲缘关系的进程
int pipefd[2];
pipe(pipefd); // pipefd[0] 读端,pipefd[1] 写端
if (fork() == 0) {
// 子进程:写数据
close(pipefd[0]);
write(pipefd[1], "Hello", 5);
close(pipefd[1]);
} else {
// 父进程:读数据
close(pipefd[1]);
char buf[10];
read(pipefd[0], buf, 5);
close(pipefd[0]);
}特点:直接映射到进程地址空间,无需数据拷贝,但需要同步机制
// 创建共享内存
int shmid = shmget(IPC_PRIVATE, 1024, 0666 | IPC_CREAT);
// 映射到进程地址空间
char *shmaddr = shmat(shmid, NULL, 0);
// 写入数据
strcpy(shmaddr, "Hello");
// 读取数据(另一个进程)
printf("%s\n", shmaddr);
// 解除映射
shmdt(shmaddr);特点:消息有类型,可以选择性接收,异步通信
// 创建消息队列
int msgid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
// 发送消息
struct msgbuf {
long mtype;
char mtext[100];
};
struct msgbuf msg = {1, "Hello"};
msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
// 接收消息
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);ls | grep)多个线程访问共享资源时,如果不加控制,会导致竞态条件(Race Condition),产生不可预期的结果。
| 机制 | 特点 | 适用场景 |
|---|---|---|
| 互斥锁(Mutex) | 保证同一时刻只有一个线程访问临界区 | 保护共享资源 |
| 读写锁(RWLock) | 允许多个读者或一个写者 | 读多写少场景 |
| 信号量(Semaphore) | 控制同时访问资源的线程数量 | 资源池、限流 |
| 条件变量(Condition) | 线程间通知机制 | 生产者-消费者模型 |
| 自旋锁(Spinlock) | 忙等待,不释放 CPU | 临界区很短的场景 |
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* thread_func(void* arg) {
pthread_mutex_lock(&mutex);
// 临界区:访问共享资源
shared_counter++;
pthread_mutex_unlock(&mutex);
return NULL;
}sem_t sem;
sem_init(&sem, 0, 3); // 初始值为 3,最多 3 个线程同时访问
void* thread_func(void* arg) {
sem_wait(&sem); // P 操作,信号量 -1
// 访问资源
sem_post(&sem); // V 操作,信号量 +1
return NULL;
}pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int buffer[10];
int count = 0;
// 生产者
void* producer(void* arg) {
pthread_mutex_lock(&mutex);
while (count == 10) {
pthread_cond_wait(&cond, &mutex); // 缓冲区满,等待
}
buffer[count++] = produce_item();
pthread_cond_signal(&cond); // 通知消费者
pthread_mutex_unlock(&mutex);
}
// 消费者
void* consumer(void* arg) {
pthread_mutex_lock(&mutex);
while (count == 0) {
pthread_cond_wait(&cond, &mutex); // 缓冲区空,等待
}
int item = buffer[--count];
pthread_cond_signal(&cond); // 通知生产者
pthread_mutex_unlock(&mutex);
}CPU 从一个进程/线程切换到另一个进程/线程时,需要保存当前执行状态并恢复目标状态的过程。
| 类型 | 开销 | 原因 |
|---|---|---|
| 进程切换 | 大 | 需要切换虚拟内存空间、TLB 全部失效 |
| 线程切换(同进程) | 小 | 共享地址空间,只需切换栈和寄存器 |
| 协程切换 | 极小 | 用户态切换,只需保存栈指针 |
# Linux 查看系统级上下文切换
vmstat 1
# cs 列:每秒上下文切换次数
# 查看进程级上下文切换
pidstat -w -p 1
# cswch/s:自愿上下文切换(I/O 等待)
# nvcswch/s:非自愿上下文切换(时间片用完) 某 Java 服务 QPS 从 5000 提升到 8000 的优化:
子进程已经终止,但父进程还没有调用 wait() 或 waitpid() 回收其资源,导致子进程的 PCB 仍然保留在系统中。
Z(Zombie)// 方法 1:父进程调用 wait() 回收子进程
pid_t pid = fork();
if (pid == 0) {
// 子进程
exit(0);
} else {
// 父进程
wait(NULL); // 阻塞等待子进程结束
}
// 方法 2:使用 SIGCHLD 信号处理
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0); // 非阻塞回收
}
signal(SIGCHLD, sigchld_handler);
// 方法 3:两次 fork(),让孙子进程成为孤儿进程
if (fork() == 0) {
if (fork() == 0) {
// 孙子进程,父进程立即退出,由 init 回收
sleep(10);
exit(0);
}
exit(0); // 子进程立即退出
}
wait(NULL); // 回收子进程父进程先于子进程终止,子进程成为孤儿进程,会被 init 进程(PID 1)收养并自动回收。
# 查看僵尸进程
ps aux | grep Z
# 或
ps -eo pid,ppid,stat,cmd | grep Z
# 查看僵尸进程数量
ps aux | grep Z | wc -l
# 杀死僵尸进程的父进程(僵尸进程无法直接 kill)
kill -9 <父进程PID>某服务器出现大量僵尸进程,导致无法创建新进程:
多个线程同时访问共享资源,结果依赖于线程执行的时序。
// 问题代码
class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作:读取 -> 加 1 -> 写回
}
}
// 解决方案 1:synchronized
public synchronized void increment() {
count++;
}
// 解决方案 2:AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}两个或多个线程互相等待对方持有的资源,导致永久阻塞。
// 问题代码
synchronized(lockA) {
synchronized(lockB) {
// 线程 1
}
}
synchronized(lockB) {
synchronized(lockA) {
// 线程 2:可能死锁
}
}
// 解决方案:按固定顺序获取锁
synchronized(lockA) {
synchronized(lockB) {
// 所有线程都按 A -> B 顺序
}
}线程不断重试但无法继续执行,类似于两个人在走廊相遇,都试图让路但始终挡住对方。
解决方案:引入随机延迟
某些线程长期无法获得资源,一直处于等待状态。
解决方案:使用公平锁(ReentrantLock 的公平模式)
// 问题代码
class Task {
private boolean stop = false;
public void run() {
while (!stop) {
// 可能永远看不到 stop 的变化
}
}
public void stop() {
stop = true;
}
}
// 解决方案:使用 volatile
private volatile boolean stop = false;多个线程修改同一缓存行中的不同变量,导致缓存失效。
// 问题代码
class Counter {
long count1; // 可能在同一缓存行
long count2;
}
// 解决方案:缓存行填充
class Counter {
long count1;
long p1, p2, p3, p4, p5, p6, p7; // 填充到不同缓存行
long count2;
}由用户空间的线程库管理,内核不感知其存在。
由操作系统内核管理和调度。
| 模型 | 映射关系 | 特点 | 代表 |
|---|---|---|---|
| 多对一(N:1) | 多个用户线程映射到一个内核线程 | 无法利用多核 | 早期 Java Green Threads |
| 一对一(1:1) | 每个用户线程对应一个内核线程 | 开销大,但可利用多核 | Linux NPTL、Windows |
| 多对多(M:N) | 多个用户线程映射到多个内核线程 | 兼顾性能和并发 | Go goroutine、Erlang |
G(Goroutine):用户态线程
M(Machine):内核线程
P(Processor):调度器
P1 P2
↓ ↓
G1 G2 G3 G4 G5 G6
↓ ↓
M1 M2
↓ ↓
内核线程 内核线程Java 19 引入的虚拟线程,采用 M:N 模型,可以创建百万级线程。
// 传统线程
Thread thread = new Thread(() -> {
// 任务
});
thread.start();
// 虚拟线程(Java 19+)
Thread.startVirtualThread(() -> {
// 任务
});ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 空闲线程存活时间
TimeUnit.SECONDS,
workQueue, // 任务队列
threadFactory, // 线程工厂
rejectedHandler // 拒绝策略
);1. 提交任务
↓
2. 核心线程数未满?
是 → 创建核心线程执行
否 → 继续
↓
3. 队列未满?
是 → 加入队列等待
否 → 继续
↓
4. 最大线程数未满?
是 → 创建非核心线程执行
否 → 执行拒绝策略CPU 密集型任务:
I/O 密集型任务:
// CPU 密集型:图像处理
int cpuCount = Runtime.getRuntime().availableProcessors();
ExecutorService cpuPool = Executors.newFixedThreadPool(cpuCount + 1);
// I/O 密集型:HTTP 请求
ExecutorService ioPool = Executors.newFixedThreadPool(cpuCount * 2);
// 混合型:根据实际测试调整
ThreadPoolExecutor mixedPool = new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);ThreadPoolExecutor executor = ...;
// 监控指标
executor.getActiveCount(); // 活跃线程数
executor.getPoolSize(); // 当前线程数
executor.getQueue().size(); // 队列中任务数
executor.getCompletedTaskCount(); // 已完成任务数协程是用户态的轻量级线程,由程序自己调度,不需要内核参与,可以在单个线程内实现并发。
| 特性 | 线程 | 协程 |
|---|---|---|
| 调度方式 | 内核抢占式调度 | 用户态协作式调度 |
| 切换开销 | 较大(需要内核态切换) | 极小(用户态切换) |
| 并发数量 | 数千(受限于内存) | 数万甚至百万 |
| 栈大小 | 固定(MB 级) | 动态(KB 级) |
| 同步问题 | 需要锁 | 不需要(单线程内) |
| 利用多核 | 可以 | 需要配合多线程 |
# 协程执行流程
协程 A 协程 B
↓ ↓
执行代码 等待
↓ ↓
遇到 I/O,主动让出 ↓
↓ ─────────────→ 获得执行权
等待 执行代码
↓ ↓
↓ 遇到 I/O,主动让出
↓ ←───────────── 等待
获得执行权 ↓
↓ ↓
继续执行 等待import asyncio
async def fetch_data(url):
print(f"开始请求 {url}")
await asyncio.sleep(1) # 模拟 I/O,主动让出
print(f"完成请求 {url}")
return f"Data from {url}"
async def main():
# 并发执行多个协程
tasks = [
fetch_data("url1"),
fetch_data("url2"),
fetch_data("url3")
]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
# 输出:
# 开始请求 url1
# 开始请求 url2
# 开始请求 url3
# (1 秒后)
# 完成请求 url1
# 完成请求 url2
# 完成请求 url3package main
import (
"fmt"
"time"
)
func fetchData(url string, ch chan string) {
fmt.Printf("开始请求 %s\n", url)
time.Sleep(1 * time.Second)
fmt.Printf("完成请求 %s\n", url)
ch <- fmt.Sprintf("Data from %s", url)
}
func main() {
ch := make(chan string, 3)
// 启动 3 个协程
go fetchData("url1", ch)
go fetchData("url2", ch)
go fetchData("url3", ch)
// 接收结果
for i := 0; i < 3; i++ {
fmt.Println(<-ch)
}
}某爬虫系统使用 Python asyncio 协程: