跳到主要内容

进程 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 过度订阅或连接池耗尽。

一个实用的选择流程​

  1. 任务主要在等待网络、文件或数据库吗?是的话先考虑线程或异步 I/O。
  2. 任务主要消耗 CPU,且可以拆成相互独立的批次吗?是的话测试多进程。
  3. 是否必须共享大量可变状态?共享越多,进程通信或线程锁的成本越高,应重新设计数据流。
  4. 单个任务是否足够大,能覆盖启动和通信成本?若不能,合并任务或采用串行处理。
  5. 是否需要隔离不可信代码、第三方库崩溃或内存泄漏?优先考虑进程边界。

最后用基准测试验证:分别测量任务本身、调度、序列化、通信、内存和外部服务限流,而不要只比较总耗时一个数字。

常见误区​

  • “线程一定比进程快”:线程启动通常更轻,但共享同步和 GIL 可能让它在 CPU 任务中更慢。
  • “用了多个线程就用满多核”:CPython 普通 Python 字节码受 GIL 约束。
  • “进程之间完全不能通信”:可以通过 IPC、共享内存或管理器通信,只是必须显式设计。
  • “加大 worker 数量就会加速”:过多 worker 会增加切换、内存、锁竞争或外部服务压力。