多进程、多线程与进程池线程池

多进程、多线程与进程池线程池

时间:2026/05/04

关键词:forkexec、僵尸进程、管道、共享内存、消息队列、pthread、互斥锁、条件变量、进程池、线程池
核心目标:理解 Linux 服务端常见并发模型,知道什么时候用进程、线程、进程池或线程池。


1. 为什么服务器需要并发模型

单连接阻塞式服务器只能处理一个客户端。
真实服务端必须同时面对:

  • 多个连接
  • 慢客户端
  • CPU 密集业务
  • 阻塞 I/O
  • 超时和取消

常见并发方式:

  • 多进程
  • 多线程
  • I/O 复用 + 单线程 Reactor
  • I/O 线程 + worker 线程池
  • 进程池 / 线程池

没有一种模型永远最好,关键看隔离性、开销和状态共享方式。


2. fork:创建子进程

1
2
3
#include <unistd.h>

pid_t pid = fork();

返回值:

返回 位置
pid > 0 父进程,返回子进程 pid
pid == 0 子进程
pid < 0 创建失败

fork 后:

  • 子进程获得父进程地址空间的副本
  • 现代系统通常用写时复制
  • 文件描述符会被继承

服务端常见模式:

  • 父进程负责监听和管理
  • 子进程负责处理连接
  • 或多个子进程共同 accept

3. exec:替换进程映像

exec 系列会用新程序替换当前进程:

1
execl("/bin/ls", "ls", "-l", NULL);

常见用途:

  • CGI 子进程执行外部程序
  • worker 进程启动其他服务
  • 守护进程重启自身

注意:

  • exec 成功后不会返回
  • 未设置 FD_CLOEXEC 的 fd 会被继承到新程序
  • 这也是为什么服务端要重视 close-on-exec

4. 僵尸进程

子进程退出后,父进程如果不回收它的退出状态,就会留下僵尸进程。

常用回收:

1
2
3
4
#include <sys/wait.h>

while (waitpid(-1, NULL, WNOHANG) > 0) {
}

通常在 SIGCHLD 到来后回收。
但信号处理函数里不应做复杂逻辑,更稳妥的是:

  • 设置标志
  • 写 pipe/eventfd
  • 在主循环里 waitpid

5. 进程间通信方式

常见 IPC:

方式 特点
pipe 简单字节流,适合父子进程
Unix domain socket 支持双向通信,可传 fd
共享内存 速度快,但需要同步
信号量 进程间同步
消息队列 内核维护消息
eventfd 轻量计数通知

高性能服务端里常见组合:

  • 共享内存存数据
  • eventfd 或信号量做通知
  • Unix domain socket 传递 fd

6. 在进程间传递文件描述符

Unix domain socket 可以通过辅助数据传 fd。
这很适合:

  • master 进程 accept
  • 把连接 fd 分发给 worker 进程

直觉是:

1
2
3
master accept conn_fd
-> sendmsg 把 conn_fd 发给 worker
worker recvmsg 得到可用 fd

传递的是“打开文件描述”的引用,不是把 socket 数据复制一份。

这种模型能让 master 集中管理监听,而 worker 专注处理连接。


7. 多进程模型的优缺点

优点:

  • 进程间隔离强
  • 一个 worker 崩溃不一定拖垮全部
  • 适合利用多核
  • 和权限隔离更自然

缺点:

  • 进程创建和切换成本高于线程
  • 共享状态复杂
  • IPC 成本和代码复杂度更高
  • 多进程共同 accept 需要考虑惊群和负载分配

适合:

  • 强隔离服务
  • CGI / worker 模型
  • 多租户或不可信任务

8. pthread 基础

创建线程:

1
2
3
4
#include <pthread.h>

pthread_t tid;
pthread_create(&tid, NULL, thread_func, arg);

等待线程退出:

1
pthread_join(tid, NULL);

分离线程:

1
pthread_detach(tid);

工程建议:

  • 能 join 就明确 join
  • 不要让线程生命周期变成“没人知道它还在不在”
  • 服务端更常用线程池,而不是每个请求创建一个线程

9. 互斥锁、条件变量与信号量

互斥锁保护共享状态:

1
2
3
pthread_mutex_lock(&mutex);
// critical section
pthread_mutex_unlock(&mutex);

条件变量用于等待某个条件成立:

1
2
3
4
5
pthread_mutex_lock(&mutex);
while (queue_empty()) {
pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);

注意必须用 while,不是 if
原因是:

  • 可能有虚假唤醒
  • 被唤醒后条件也可能被其他线程抢先改变

信号量适合表达资源计数:

1
2
sem_wait(&sem);
sem_post(&sem);

10. 多线程环境里的常见问题

10.1 可重入函数

可重入函数可以在并发或信号打断场景下安全再次进入。
服务端要避免使用隐藏全局状态的旧接口,优先使用带 _r 或现代替代接口。

10.2 线程和信号

信号是发给进程的,但会由某个线程处理。
多线程服务里通常做法是:

  • 所有工作线程阻塞相关信号
  • 专门线程或 event loop 使用 signalfd 统一处理

10.3 死锁

典型原因:

  • 锁顺序不一致
  • 持锁调用外部回调
  • 持锁做阻塞 I/O
  • 忘记异常或错误路径解锁

经验规则:

  • 锁粒度小
  • 锁顺序固定
  • 不持锁做慢操作
  • 能用消息传递就少共享状态

11. 为什么需要池

如果每来一个连接或请求都创建进程/线程,成本会很高:

  • 创建销毁开销
  • 栈和内核资源开销
  • 调度抖动
  • 高峰期不可控

池的思想是:

预先创建一组 worker,任务来了就投递,worker 循环处理。

池能带来:

  • 控制并发上限
  • 减少创建销毁成本
  • 让资源使用可预测

12. 进程池模型

典型模型:

1
2
3
4
5
6
7
8
9
master
-> 创建 N 个 worker 进程
-> 监听 socket
-> 分发连接或任务

worker
-> 接收任务
-> 处理请求
-> 返回结果

分发方式:

  • master accept 后传 fd 给 worker
  • 多 worker 共享监听 socket,自行 accept
  • SO_REUSEPORT 下多个进程各自监听同端口

优点:

  • 隔离强
  • worker 崩溃可重启

难点:

  • IPC
  • 负载均衡
  • 共享状态
  • 优雅退出

13. 线程池模型

典型线程池结构:

1
2
3
4
任务队列
-> worker threads
-> pop task
-> execute

基本组件:

  • 任务队列
  • mutex
  • condition variable
  • 停止标志
  • worker 数组

服务端里更常见的是:

1
2
3
4
5
I/O 线程解析请求
-> 投递业务任务到线程池
worker 处理
-> 结果回投给 I/O 线程
I/O 线程发送响应

不要让 worker 随意直接操作 socket。
连接状态最好由固定 I/O 线程拥有。


14. 半同步/半异步与半同步/半反应堆

可以粗略理解:

  • 异步层:负责 I/O 事件和连接接入
  • 同步层:负责业务任务处理
  • 队列:连接两层

半同步/半反应堆的常见形态:

1
2
3
4
5
6
7
8
9
10
11
12
Reactor thread:
epoll_wait
read request
push task

Worker threads:
pop task
process
push response

Reactor thread:
send response

这个模型比“每连接一个线程”更可控,也比“所有业务都在 I/O 线程”更稳。


15. 池化模型常见坑

15.1 任务队列无限长

请求处理不过来时,内存会先被任务队列吃掉。
必须有队列上限和拒绝策略。

15.2 CPU 任务和阻塞 I/O 混在一个池

慢 I/O 会占满线程,导致 CPU 任务也排队。
建议拆不同线程池。

15.3 worker 直接关闭连接

这会破坏连接 owner 模型。
更稳妥的是发消息给 I/O 线程,让它关闭。

15.4 没有优雅退出

线程池退出要考虑:

  • 不再接收新任务
  • 唤醒所有 worker
  • 处理或丢弃队列剩余任务
  • join worker

15.5 多进程没有处理子进程崩溃

master 要能发现 worker 退出,并按策略重启或降级。


16. 一页总结

这篇最值得记住的是:

  1. 多进程隔离强,但 IPC 和资源管理复杂
  2. 多线程共享方便,但要面对锁、竞态和死锁
  3. 僵尸进程必须用 waitpid 回收
  4. 条件变量等待条件要用 while
  5. 池化是为了控制并发上限和降低创建销毁成本
  6. 服务端常用 I/O 线程管连接,worker 池管业务
  7. 队列上限、超时、拒绝策略和优雅退出必须一起设计

如果只记一句:

并发模型不是线程越多越好,而是让连接归属、任务队列、资源上限和失败处理都可控。


17. 参考资料

  1. Linux man-pages: fork
    https://man7.org/linux/man-pages/man2/fork.2.html

  2. Linux man-pages: waitpid
    https://man7.org/linux/man-pages/man2/waitpid.2.html

  3. pthreads manual
    https://man7.org/linux/man-pages/man7/pthreads.7.html

  4. Linux man-pages: unix domain sockets
    https://man7.org/linux/man-pages/man7/unix.7.html