进程 vs 线程
进程和线程都是操作系统管理的执行单元,但职责不同:进程更像资源隔离的容器,线程更像容器中的执行路线。一个进程至少包含一个线程,也可以包含多个线程。
在 Python 中,选择进程还是线程不能只看“哪个更快”,还要看任务是 CPU 密集还是 I/O 密集、数据是否需要共享、是否需要故障隔离,以及通信和启动成本能否接受。
核心差异
| 比较项 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立,默认不共享普通对象 | 同一进程内共享内存 |
| 启动成本 | 通常更高 | 通常更低 |
| 数据交换 | 队列、管道、共享内存、代理,常需序列化 | 直接访问对象,但需同步 |
| 故障影响 | 进程崩溃通常不直接破坏其他进程 | 未处理异常、数据竞争可能影响整个进程 |
| CPython 普通字节码 | 多个进程可利用多个 CPU 核心 | 受 GIL 限制,不能靠线程获得纯 Python CPU 并行 |
| 典型场景 | CPU 密集、隔离性要求高 | 网络/文件/数据库等待、共享少量状态 |
“并发”表示任务在同一时间段内推进;“并行”表示任务在同一时刻使用多个计算资源。多线程可以提高 I/O 并发,却不等于普通 Python 计算会在多个核心上并行。
根据任务类型选择
I/O 密集型:优先线程或异步 I/O
网络请求、磁盘读写、数据库调用的大部分时间在等待外部系统。线程在等待时可以让出执行权,其他任务继续:
from concurrent.futures import ThreadPoolExecutor
def request(url: str) -> str:
# 真实代码中应使用带超时的 HTTP 客户端
return f"响应:{url}"
with ThreadPoolExecutor(max_workers=8) as executor:
responses = list(executor.map(request, ["/a", "/b", "/c"]))
如果任务数量很多、I/O 库支持 async,异步 I/O 可以用较少线程处理大量等待;但它要求调用链遵守异步模型。
CPU 密集型:考虑多进程
图像处理、压缩、解析、搜索和数值计算会持续占用 CPU。CPython 中,多个进程拥有各自的解释器和 GIL,可以利用多个核心:
from concurrent.futures import ProcessPoolExecutor
def calculate(number: int) -> int:
return sum(i * i for i in range(number))
if __name__ == "__main__":
with ProcessPoolExecutor() as executor:
print(list(executor.map(calculate, [10_000, 20_000, 30_000])))
进程不是免费加速:参数和结果通常需要序列化,进程启动与数据复制也要花时间。任务过小或需要传输大量数据时,收益可能被通信成本抵消。
通信与安全边界
线程共享对象,所以传数据简单,但共享也带来竞态条件、死锁和意外修改。应使用 Lock、Queue 或不可变对象表达同步关系。
进程默认隔离内存,子进程修改普通变量不会改变父进程变量。它更安全、更适合故障隔离,却必须通过显式 IPC 交换数据。multiprocessing.Queue、Pipe 和 ProcessPoolExecutor 适合传递可序列化结果;共享内存适合高频传输但需要更严格的同步设计。
内存与资源继承
线程共享模块全局变量、打开的文件描述符和连接对象,但“能够访问”不代表“允许并发使用”。例如数据库连接是否支持多线程应以驱动文档为准。
子进程的继承行为取决于 spawn、fork 或 forkserver 启动方式。跨平台代码不应假设子进程天然拥有父进程的运行状态;目标函数、参数和返回值应按可序列化边界设计,并始终保护程序入口。
生命周期与错误处理
两种模型都需要明确管理生命周期:
- 启动后保存句柄,使用
join()或 Future 等待; - 设置网络、队列和 Future 的超时,避免无限等待;
- 捕获并记录任务异常;
- 在
finally中关闭文件、连接和线程池/进程池; - 不要依赖守护线程或强制终止来完成数据落盘。
线程池任务抛出的异常会在 future.result() 时重新抛出;进程池还会受启动方式、目标函数可序列化和 if __name__ == "__main__" 入口保护影响。
可以组合使用吗
可以。例如主进程使用少量进程分摊 CPU 计算,每个进程再用线程等待网络或磁盘。但组合模型会放大 worker 数量、连接数和内存占用,也更难关闭与定位错误。只有测量表明单一模型不足时才增加这一层复杂度。
许多数据库、Web 服务器和科学计算库已经在内部管理线程或进程。再次套一层并发前,应先确认底层实现,否则可能出现 CPU 过度订阅或连接池耗尽。
一个实用的选择流程
- 任务主要在等待网络、文件或数据库吗?是的话先考虑线程或异步 I/O。
- 任务主要消耗 CPU,且可以拆成相互独立的批次吗?是的话测试多进程。
- 是否必须共享大量可变状态?共享越多,进程通信或线程锁的成本越高,应重新设计数据流。
- 单个任务是否足够大,能覆盖启动和通信成本?若不能,合并任务或采用串行处理。
- 是否需要隔离不可信代码、第三方库崩溃或内存泄漏?优先考虑进程边界。
最后用基准测试验证:分别测量任务本身、调度、序列化、通信、内存和外部服务限流,而不要只比较总耗时一个数字。
常见误区
- “线程一定比进程快”:线程启动通常更轻,但共享同步和 GIL 可能让它在 CPU 任务中更慢。
- “用了多个线程就用满多核”:CPython 普通 Python 字节码受 GIL 约束。
- “进程之间完全不能通信”:可以通过 IPC、共享内存或管理器通信,只是必须显式设计。
- “加大 worker 数量就会加速”:过多 worker 会增加切换、内存、锁竞争或外部服务压力。