Skip to content

进程 vs 线程:多任务编程的"左右护法"

引言:餐厅扩张的两种思路

前面几篇我们分别讲了多进程和多线程。现在回到一个根本问题:要实现多任务,到底该用多进程还是多线程?

想象你开了一家餐厅,生意越来越好,一个人忙不过来了。你有两种扩张方案:

  • 方案 A:多开几家分店(多进程)——每家店独立运营,互不影响,一家着火不连累别家;
  • 方案 B:一家店里多雇几个厨师(多线程)——共享同一个厨房,省租金,但一个人把厨房点了,全店遭殃。

两种方案各有优劣,没有绝对的好坏,关键看你的业务场景。

这一篇,我们从 Master-Worker 模式讲起,对比多进程和多线程的优缺点,再深入线程切换的开销、计算密集 vs IO 密集的区别,最后引出异步 IO 这个"终极方案"。


一、Master-Worker 模式:多任务的通用架构

1.1 什么是 Master-Worker?

无论是多进程还是多线程,实现多任务的常用设计模式都是 Master-Worker

  • Master(老板):负责接收任务、分配任务、监督进度;
  • Worker(工人):负责执行具体任务,干完活向 Master 汇报。

生活化理解:就像建筑工地的包工头和工人。包工头不搬砖,只负责接活、派活、验收;工人只管埋头干活。

1.2 多进程版 Master-Worker

┌─────────────────┐
│   Master 进程    │  ← 监听端口,接收请求
│  (分配任务)     │
└────────┬────────┘
         │ fork()
    ┌────┴────┐
    ▼         ▼
┌───────┐ ┌───────┐
│Worker1│ │Worker2│  ← 各自独立处理请求
│进程   │ │进程   │
└───────┘ └───────┘
  • Master 进程是"老板",Worker 进程是"分店店长";
  • 每个 Worker 有独立的内存空间,一个崩溃不影响其他;
  • 典型代表:Apache 服务器(早期版本)。

1.3 多线程版 Master-Worker

┌─────────────────────────┐
│      Master 线程         │  ← 接收任务,放入队列
│      (分配任务)         │
└───────────┬─────────────┘
            │ 启动线程
    ┌───────┼───────┐
    ▼       ▼       ▼
┌──────┐┌──────┐┌──────┐
│Worker││Worker││Worker│  ← 共享内存,处理任务
│线程1 ││线程2 ││线程3 │
└──────┘└──────┘└──────┘
  • Master 线程是"店长",Worker 线程是"厨师";
  • 所有线程共享同一个进程的内存;
  • 典型代表:IIS 服务器(Windows)。

二、多进程 vs 多线程:优缺点大 PK

2.1 多进程的优点:稳定性为王

优点说明
稳定性极高一个子进程崩溃,不影响主进程和其他子进程
真正利用多核每个进程独立运行在不同 CPU 核心上
内存隔离安全进程间内存不共享,天然避免数据竞争

经典案例:Apache 早期采用多进程模型,一个请求处理进程崩溃,服务器整体照常运行。

2.2 多进程的缺点:代价高昂

缺点说明
创建代价大Windows 下创建进程开销巨大;Unix 的 fork() 相对轻量
内存占用高每个进程有独立的内存空间,几千个进程内存就爆了
调度困难操作系统同时运行几千个进程,调度都成问题
通信复杂进程间通信(IPC)需要管道、队列、共享内存等机制

2.3 多线程的优点:轻便快捷

优点说明
创建速度快线程创建比进程快得多
内存开销小共享进程内存,几千个线程也能扛住
通信方便直接读写共享变量,不需要 IPC
切换成本低线程切换比进程切换快

2.4 多线程的缺点:一损俱损

缺点说明
稳定性差任何一个线程崩溃,整个进程崩溃
数据竞争共享内存需要复杂的锁机制
GIL 限制Python 的 GIL 导致无法真正利用多核
调试困难多线程 bug 难以复现和定位

经典案例:Windows 下常见"该程序执行了非法操作,即将关闭"——往往是一个线程出问题,整个程序被强制结束。

2.5 对比总结表

对比项多进程多线程
稳定性⭐⭐⭐⭐⭐ 一个崩溃不影响其他⭐⭐ 一个崩溃全完蛋
创建速度⭐⭐ 慢(尤其 Windows)⭐⭐⭐⭐⭐ 快
内存开销⭐⭐ 大⭐⭐⭐⭐⭐ 小
多核利用⭐⭐⭐⭐⭐ 真正并行⭐⭐ 受 GIL 限制(Python)
数据共享⭐⭐ 需要 IPC⭐⭐⭐⭐⭐ 直接共享
编程复杂度⭐⭐⭐⭐ 相对简单⭐⭐ 锁机制复杂

2.6 混合模式:鱼与熊掌兼得

现代服务器通常采用多进程 + 多线程的混合模式:

┌─────────────────────────────────┐
│         Master 进程              │
│      (监听端口,分发请求)        │
└─────────────┬───────────────────┘
              │ fork()
    ┌─────────┼─────────┐
    ▼         ▼         ▼
┌───────┐ ┌───────┐ ┌───────┐
│Worker │ │Worker │ │Worker │  ← 多个进程,利用多核
│进程1  │ │进程2  │ │进程3  │
│(多线程)│ │(多线程)│ │(多线程)│  ← 每个进程内多线程,提高并发
└───────┘ └───────┘ └───────┘
  • Apache 的 worker MPMNginxIIS 都采用这种模式;
  • 既利用多核(多进程),又减少内存开销(多线程);
  • 一个线程崩溃,只影响当前进程,其他进程照常运行。

三、线程切换:看不见的"时间小偷"

3.1 一个学生的作业困境

假设你要准备中考,今晚有 5 科作业:语文、数学、英语、物理、化学,每科需要 1 小时。

方案一:单任务模式(批处理)

  • 先做完语文(1小时),再做数学(1小时)……
  • 总共 5 小时,依次完成。

方案二:多任务模式(交替做)

  • 做 1 分钟语文 → 切换 → 做 1 分钟数学 → 切换 → ……
  • 看起来在"同时"做 5 科,但切换有代价!

3.2 切换的代价

从语文切换到数学,你需要:

  1. 保存现场:合上语文书、收起钢笔、记住做到哪页;
  2. 准备新环境:打开数学书、拿出圆规直尺、找到上次做到的位置。

操作系统切换线程/进程时也一样:

  • 保存现场:保存 CPU 寄存器状态、程序计数器、内存页表;
  • 恢复现场:加载新任务的寄存器、切换内存页、刷新缓存。

3.3 切换开销的量化

操作大致耗时
线程切换约 1~10 微秒
进程切换约 10~100 微秒
缓存失效额外开销,可能更严重

看起来很快?但如果有 几千个线程 在切换:

假设 5000 个线程,每个时间片 10 毫秒
每秒钟切换次数 = 5000 × (1000ms / 10ms) = 500,000 次
每次切换开销 5 微秒
总开销 = 500,000 × 5μs = 2.5 秒/秒

CPU 花了 2.5 秒在切换上,只剩 0.5 秒真正干活!

3.4 系统假死的真相

你有没有遇到过:

  • 电脑开了很多程序,突然卡住,硬盘狂响,鼠标点不动;
  • 任务管理器里 CPU 占用 100%,但什么都没干。

这就是切换开销超过实际工作的极端情况——操作系统忙着在几千个任务间切换,根本没时间真正执行任务。

结论:多任务不是越多越好,超过限度后效率急剧下降。


四、计算密集型 vs IO 密集型:选择的关键

4.1 两种任务类型

类型特点例子
计算密集型大量计算,CPU 满负荷圆周率计算、视频编码、科学模拟
IO 密集型大量等待,CPU 空闲网络请求、文件读写、数据库查询

4.2 计算密集型任务

特点:任务的大部分时间在"算",CPU 是瓶颈。

多任务策略

  • 任务数量 ≈ CPU 核心数,多了反而降低效率(切换开销);
  • Python 不适合!用 C/C++/Rust 等编译型语言。

生活化理解:就像考试做数学题,你必须全神贯注,不能一会儿做数学一会儿背英语。多个人(多进程)同时做不同的卷子可以,但一个人频繁切换科目效率极低。

4.3 IO 密集型任务

特点:任务的大部分时间在"等"(等网络、等磁盘、等数据库),CPU 很闲。

多任务策略

  • 任务可以很多,因为等待时 CPU 可以处理其他任务;
  • Python 很合适!开发效率高,运行效率不是瓶颈。

生活化理解:就像餐厅服务员,大部分时间在等客人点菜、等厨师做菜。一个服务员可以同时照看 10 桌客人,因为 90% 的时间都在"等"。

4.4 为什么 Python 适合 IO 密集型?

计算密集型任务,代码运行效率至关重要:

python
# Python 计算 1 亿次循环:约 10 秒
# C 语言计算 1 亿次循环:约 0.1 秒
# 差距 100 倍!

IO 密集型任务,99% 时间在等 IO:

python
# 网络请求耗时 100ms,其中 CPU 计算 0.1ms,等待 99.9ms
# Python 和 C 的差距可以忽略不计
# 但 Python 开发效率是 C 的 10 倍!

结论

  • 计算密集型 → 用 C/C++,多进程,数量 = CPU 核心数;
  • IO 密集型 → 用 Python,多线程/异步 IO,数量可以很多。

五、异步 IO:单线程的"分身术"

5.1 问题的根源

多进程/多线程的本质问题是:操作系统调度开销太大

  • 进程切换:~100 微秒;
  • 线程切换:~10 微秒;
  • 几千个任务:切换开销吞噬所有 CPU 时间。

有没有办法不切换就能同时处理多个任务?

5.2 事件驱动模型

现代操作系统提供了异步 IO(Asynchronous IO)支持:

  • 程序发起 IO 请求后,不等待,直接返回;
  • 操作系统在后台完成 IO,完成后通知程序;
  • 程序用单线程就能同时处理成千上万个 IO 请求。

生活化理解:你去餐厅吃饭,不用站在厨房门口等菜做好。你点完菜就去逛街,菜好了服务员打电话叫你。这就是"异步"——你不用"等",事情好了自然会"通知"你。

5.3 Nginx:异步 IO 的典范

Nginx 采用单进程 + 异步 IO模型:

  • 单核 CPU 上,一个 Nginx 进程可以处理数万并发连接;
  • 多核 CPU 上,启动 N 个进程(N = 核心数),充分利用多核;
  • 没有线程切换开销,效率极高。

对比 Apache(多进程/多线程):

服务器模型1 万并发需要
Apache多进程几千个进程,内存爆炸
Nginx异步 IO几个进程,轻松应对

5.4 Python 的异步方案:协程

Python 的异步编程模型叫协程(Coroutine)

python
import asyncio

async def fetch_data():
    print('开始请求')
    await asyncio.sleep(1)  # 模拟 IO 等待,不阻塞线程
    print('请求完成')
    return '数据'

async def main():
    # 同时发起 3 个请求,单线程并发
    results = await asyncio.gather(
        fetch_data(),
        fetch_data(),
        fetch_data()
    )
    print(results)

asyncio.run(main())

关键点

  • await 时释放 CPU,让其他协程执行;
  • 单线程实现高并发,没有切换开销;
  • 我们将在后续章节详细讲解协程。

六、如何选择:决策流程图

你的任务类型是什么?

├─ 计算密集型(大量计算)
│   │
│   ├─ 需要极致性能? → 用 C/C++,多进程,数量 = CPU 核心数
│   │
│   └─ 性能要求一般? → Python 多进程(避开 GIL)

└─ IO 密集型(网络/文件/数据库)

    ├─ 并发量不大(< 1000)? → Python 多线程即可

    ├─ 并发量大(> 1000)? → Python 协程(asyncio)

    └─ 超大规模(C10K+)? → 上 Nginx + 多进程 + 协程

七、常见误区解析

误区 1:多线程一定比单线程快

真相:对于计算密集型任务,Python 多线程可能比单线程更慢(GIL + 切换开销)。

对于 IO 密集型任务,多线程确实更快,但收益递减——1000 个线程不一定比 100 个快 10 倍。

误区 2:进程/线程越多越好

真相:超过限度后,切换开销吞噬所有收益。

经验法则:

  • CPU 密集型:数量 ≈ CPU 核心数;
  • IO 密集型:数量可以多一些,但也不要超过几千。

误区 3:多进程比多线程"高级"

真相:没有绝对的好坏,只有适合的场景。

  • 需要稳定性、利用多核 → 多进程;
  • 需要轻便、快速创建 → 多线程;
  • 需要高并发、低资源消耗 → 异步 IO。

误区 4:Python 不能写高并发程序

真相:Python 可以用协程实现高并发。

  • 多线程受 GIL 限制,但协程不受 GIL 影响;
  • asyncio + aiohttp 可以支撑数万并发;
  • 很多大型 Python 项目(如 Instagram)都用协程。

误区 5:异步 IO 是银弹

真相:异步 IO 编程模型复杂,回调地狱、调试困难。

  • 适合 IO 密集型、高并发场景;
  • 简单业务用多线程反而更清晰;
  • 协程需要重新学习异步编程思维。

八、实战案例

案例 1:计算密集型——多进程加速图像处理

python
import multiprocessing
import time

def process_image(image_id):
    """模拟图像处理(CPU 密集型)"""
    total = 0
    for i in range(10000000):
        total += i * i
    return f'图片{image_id}处理完成'

if __name__ == '__main__':
    start = time.time()

    # 单进程串行
    for i in range(4):
        process_image(i)
    print('单进程耗时:', time.time() - start)

    start = time.time()

    # 多进程并行(4核CPU)
    with multiprocessing.Pool(4) as pool:
        pool.map(process_image, range(4))
    print('多进程耗时:', time.time() - start)

预期结果:多进程比单进程快约 4 倍(接近线性加速)。

案例 2:IO 密集型——多线程下载

python
import threading
import requests
import time

def download(url):
    r = requests.get(url)
    return len(r.content)

urls = ['https://httpbin.org/delay/1'] * 10  # 模拟 10 个慢请求

# 单线程
start = time.time()
for url in urls:
    download(url)
print('单线程耗时:', time.time() - start)  # 约 10 秒

# 多线程
start = time.time()
threads = []
for url in urls:
    t = threading.Thread(target=download, args=(url,))
    t.start()
    threads.append(t)
for t in threads:
    t.join()
print('多线程耗时:', time.time() - start)  # 约 1 秒

预期结果:多线程比单线程快约 10 倍(IO 等待时间重叠)。

案例 3:协程——单线程高并发

python
import asyncio
import aiohttp
import time

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = ['https://httpbin.org/delay/1'] * 100

    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        print(f'完成 {len(results)} 个请求')

start = time.time()
asyncio.run(main())
print('协程耗时:', time.time() - start)  # 约 1 秒(100 个并发!)

预期结果:单线程协程处理 100 个请求,耗时约等于 1 个请求的时间。


九、小结

核心知识点一句话总结
Master-Worker多任务的标准架构:Master 分配,Worker 执行
多进程优点稳定性高、真正多核、内存隔离
多进程缺点创建慢、内存高、通信复杂
多线程优点创建快、内存小、通信方便
多线程缺点一损俱损、数据竞争、GIL 限制
切换开销任务过多时,切换比干活还耗时
计算密集型用 C/C++,多进程,数量 = CPU 核心数
IO 密集型用 Python,多线程/协程,数量可以很多
异步 IO单线程高并发的终极方案,Python 用 asyncio

记住三句话

  1. 稳定性选多进程,轻便选多线程,高并发选异步 IO
  2. 计算密集用编译型语言,IO 密集用脚本语言
  3. 任务不是越多越好,切换开销是隐形杀手

参考代码

本文所有示例代码基于廖雪峰 Python 教程的"进程 vs 线程"章节重新整理和扩充。