进程 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__':
    # 必须加这个判断,Windows系统下创建子进程会重新导入主模块
    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(Python的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会按顺序返回结果,但这里我们不需要
        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__':
    # Python 3.7+ 可以直接用asyncio.run
    asyncio.run(main())

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


9. 最后总结

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

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