进程 vs. 线程

日常用电脑时,你一定经历过这样的场景:后台挂着游戏更新,浏览器里打开 3 个编程文档、1 个在线音乐页面,同时还在微信摸鱼刷消息——这就是典型的多任务并行处理
今天我们就来拆解实现多任务的三位核心角色:进程、线程,还有在高并发场景下越来越受欢迎的协程
我们会对比它们的特点、优缺点和适用场景,最后再用 Python 写几段极简代码,让你一眼看清它们的区别。


1. 多任务的通用架构:Master‑Worker 模式

不管底层用的是多进程、多线程还是协程,大多数系统实现多任务的思路都离不开 Master‑Worker 分工协作

  • Master(监工):负责任务接收、拆分、分配,有时也做全局协调。
  • Worker(工人):接到任务后立刻执行。
  • 常规配置:一个 Master 对应多个 Worker。

具体到不同实现:

  • 多进程:主进程当 Master,子进程当 Worker。
  • 多线程:主线程当 Master,子线程当 Worker。
  • 协程:主线程(或单个进程)内的事件循环当 Master,协程当 Worker。

2. 传统双雄:多进程 vs 多线程

多进程模式

我们可以把进程理解为一个独立的“工作车间”:每个车间都有自己的仓库(内存空间)、工具库(文件句柄、网络连接等)。车间之间完全物理隔离,除非专门修一条“传送带”(进程间通信,IPC),否则不能随便拿对方的东西。

优点

  1. 稳定性天花板:某个子车间(子进程)炸了(崩溃),只会收拾它自己的摊子,主车间和其他子车间完全不受影响。这也是早期 Apache、Chrome 浏览器早期架构采用纯多进程的原因。
  2. 彻底利用多核 CPU:每个车间可以独占一个 CPU 核心干活,不受调度限制(Windows / Linux 都能做到)。
  3. 绝对的内存隔离:完全不用担心其他车间“偷拿”或“污染”自己的数据。

缺点

  1. 车间建造成本高:申请独立仓库、工具库都需要时间和系统资源。Windows 系统下的创建成本尤其大,甚至是 Linux 的数倍到数十倍。
  2. 切换成本大:车间主任(CPU 核心)从 A 车间转到 B 车间,得先把 A 的所有工具、进度条收好,再把 B 的拿出来摆好,这个过程叫上下文切换
  3. IPC 比较麻烦:车间之间传文件、数据得用专用的传送带(管道、消息队列、共享内存等)。如果使用共享内存,还得加“安全门”(锁)防止拿错,代码复杂度会明显上升。

多线程模式

线程则是车间里的“工人小组”:所有小组共享同一个仓库、工具库,但每个小组有自己的工作桌、进度条(寄存器、栈空间)。小组之间沟通,直接递纸条就行(共享内存),不需要额外的传送带。

优点

  1. 小组组建快:不需要申请独立仓库,直接在现有车间里划一块区域当工作桌就行,创建和切换成本远低于进程。
  2. 小组间沟通简单:递纸条(共享全局变量、堆内存)效率极高。
  3. 适合 I/O 等待场景:比如某个小组在等快递(等网络请求、等磁盘读写),车间主任可以立刻切换到另一个空闲小组干活,CPU 不会闲着。

缺点

  1. 稳定性较差:某个小组不小心碰断了车间的主电源(触发非法内存访问),整个车间(整个进程)都会断电。这也是很多早期 Web 服务器不敢使用纯多线程的原因。
  2. 需要处理“抢仓库”的问题:多个小组同时用同一把螺丝刀(修改同一个全局变量),会把数据改乱,这叫竞态条件。解决办法是加“工具使用登记本”(锁)来规范,但锁用多了会让效率下降,甚至出现“死锁”(两个小组各拿对方需要的螺丝刀不放)。
  3. Python 的特殊限制:CPython(我们最常用的 Python 解释器)里有个 GIL(全局解释器锁):同一时间只能有一个小组(线程)在车间的“主操作区”(执行 Python 字节码)干活。这意味着纯 Python 写的多线程,无法真正利用多核 CPU 并行处理计算任务

3. 现代服务器的妥协:混合模式

纯多进程或纯多线程都有各自的硬伤,所以主流服务器软件早就用上了 “多进程保底 + 多线程提效” 的混合模式

  • Apache:支持三种多处理模块(MPM)
    • prefork:纯多进程,适合对稳定性要求极高的场景。
    • worker:多进程打底,每个子进程里开多个线程,平衡了稳定性和资源利用率。
    • event:在 worker 的基础上优化了 I/O 等待,性能更高。
  • IIS:默认多线程,但也支持进程隔离模式。
  • Nginx:走得更远,直接用了 单 Master + 多 Worker 进程 + 事件驱动 的异步模型,性能在很多场景下碾压 Apache 和 IIS 的传统模式。

4. 别让“换工人”拖垮效率:任务切换的坑

不管是切换进程、线程,还是后面要说的协程,上下文切换本身都是有成本的

  1. 保存当前任务的进度条、工具状态。
  2. 准备下一个任务的进度条、工具状态。
  3. 把下一个任务放到 CPU 核心上开始执行。

如果任务开得太多,系统可能会把 80% 以上的时间花在切换上,只有 20% 的时间在真正干活
CPU 核心利用率看起来很高,但实际业务处理速度很慢,甚至会出现“系统假死”(鼠标键盘动不了,但后台进程还在跑)。


5. 选对人干对活:任务类型与方案匹配

在选择多任务方案前,先搞清楚你的任务是 “CPU 忙” 还是 “闲等多”

计算密集型任务(CPU 忙)

  • 特点:大部分时间都在 CPU 上运算,几乎不闲等。
    典型例子:科学计算、视频编解码、密码学哈希 / 加密、机器学习训练推理。
  • 建议
    • 任务数 ≈ CPU 核心数(可以多 1~2 个,用来处理突发的系统调度)。
    • 优先用 C / C++ / Rust / Golang 这些没有全局解释器锁、编译后效率极高的语言。
    • 如果非要用 Python,选多进程(可以绕开 GIL),或者用 C 扩展(比如 NumPy、PyTorch 底层都是 C 写的,不受 GIL 限制)。

I/O 密集型任务(闲等多)

  • 特点:大部分时间都在“闲等”:等网络请求返回、等数据库读写、等用户输入。
    CPU 核心利用率很低(可能只有 10%~30%)。
    典型例子:Web 服务、爬虫、数据库中间件、聊天软件后端。
  • 建议
    • 任务数可以适当开多(比如 CPU 核心数的 5~10 倍,甚至更多)。
    • Python / JavaScript 这些解释型脚本语言完全够用,开发效率比执行效率重要得多。
    • 现代趋势是用异步 I/O + 协程(后面会详细讲),比多线程效率更高、开销更低。

6. 高并发场景的新宠:异步 I/O + 协程

异步 I/O

现代操作系统(Linux 的 epoll、Windows 的 IOCP、macOS 的 kqueue)提供了高效的异步 I/O 支持:不需要专门开多个线程 / 进程去等待 I/O,只需要告诉操作系统“等这个网络请求回来后喊我一声”,然后主线程 / 单个进程就可以去干别的事了。
典型代表:Nginx、Node.js、Redis(主进程是单线程的,但性能极高)。

协程

协程可以理解为 “轻量级到极致的工人小组”:所有协程都在同一个线程里,由程序自己写的调度器(事件循环) 控制切换,不需要操作系统插手。
切换一个协程的成本有多低?大概是切换线程的 千分之一甚至万分之一——只需要保存和恢复几个指针就行。

Python 里的协程语法是 async / await,配套的标准库是 asyncio,还有第三方库 gevent(对同步代码更友好)。

优势

  1. 并发能力拉满:单个线程里可以轻松开几万甚至几十万个协程(多线程最多开几千个就会因为栈空间不够崩溃)。
  2. 几乎没有切换成本
  3. 不需要处理复杂的线程锁问题(因为只有一个线程在执行协程,同一时间只有一个协程在运行“主操作区”的代码)。

7. 一张表搞定选择

场景推荐方案核心原因
科学计算、视频转码多进程 / C 扩展绕开 GIL,彻底利用多核
简单的后台批量处理线程池实现简单,资源利用率足够
Web 服务、高并发爬虫协程 / 异步框架(FastAPI、aiohttp、Scrapy Asyncio)高并发、低开销、开发效率高
多租户、高稳定性要求多进程(甚至多进程 + 容器)彻底隔离,一个租户崩了不影响其他人

8. Python 实现示例(极简版)

多进程

from multiprocessing import Process
import time

def worker(task_name: str, delay: int) -> None:
    print(f"[子进程] 开始执行 {task_name}")
    time.sleep(delay)   # 模拟计算或短 I/O
    print(f"[子进程] 完成 {task_name}")

if __name__ == '__main__':
    start_time = time.time()
    tasks = [("数学计算1", 2), ("数学计算2", 3), ("数学计算3", 1)]
    processes = []

    # 创建并启动子进程
    for name, d in tasks:
        p = Process(target=worker, args=(name, d))
        processes.append(p)
        p.start()

    # 等待所有子进程完成
    for p in processes:
        p.join()

    print(f"[主进程] 所有任务完成,总耗时 {time.time() - start_time:.2f}s")

运行结果大约总耗时 3s(三个子进程并行执行)。

多线程

from threading import Thread
from concurrent.futures import ThreadPoolExecutor
import time

def worker(task_name: str, delay: int) -> None:
    print(f"[子线程] 开始执行 {task_name}")
    time.sleep(delay)   # 模拟 I/O(time.sleep 会释放 GIL)
    print(f"[子线程] 完成 {task_name}")

if __name__ == '__main__':
    start_time = time.time()
    tasks = [("爬取网页1", 2), ("爬取网页2", 3), ("爬取网页3", 1)]

    # 方式1:直接创建线程(适合任务数少的场景)
    # threads = []
    # for name, d in tasks:
    #     t = Thread(target=worker, args=(name, d))
    #     threads.append(t)
    #     t.start()
    # for t in threads:
    #     t.join()

    # 方式2:使用线程池(推荐!自动管理线程生命周期,避免频繁创建销毁)
    with ThreadPoolExecutor(max_workers=4) as executor:
        executor.map(lambda x: worker(*x), tasks)

    print(f"[主线程] 所有任务完成,总耗时 {time.time() - start_time:.2f}s")

因为是 I/O 密集型,总耗时也是 3s 左右(GIL 在 time.sleep 时会释放)。

协程

import asyncio
import time

async def worker(task_name: str, delay: int) -> None:
    print(f"[协程] 开始执行 {task_name}")
    # 必须用 asyncio.sleep,不能用 time.sleep(否则会阻塞整个事件循环)
    await asyncio.sleep(delay)
    print(f"[协程] 完成 {task_name}")

async def main() -> None:
    start_time = time.time()
    tasks = [("API请求1", 2), ("API请求2", 3), ("API请求3", 1)]

    # asyncio.gather 会并行执行所有协程
    await asyncio.gather(*(worker(name, d) for name, d in tasks))

    print(f"[主协程] 所有任务完成,总耗时 {time.time() - start_time:.2f}s")

if __name__ == '__main__':
    asyncio.run(main())

总耗时同样是 3s 左右,但资源利用率比多线程高得多。


9. 最后总结

  1. 先分任务类型:CPU 密集型找多进程 / C 扩展,I/O 密集型找协程 / 异步框架。
  2. 不要过度设计:如果只是简单的后台定时任务,线程池就足够了。
  3. Python 特殊情况:永远别忘了 GIL 的存在。
  4. 关注现代趋势:异步 I/O 和协程已经是高并发 Web 服务的主流方案。

选择最适合你需求的,而不是看起来“最厉害”的,这才是好的架构设计。