📝 进程、线程、协程:从零搞懂并发三兄弟(附实战补充)

在学习 DeepAgents 和异步编程时,进程、线程、协程总是绕不开的概念。本文用最通俗的方式帮你理清它们的区别与联系,并补充查看进程信息及 multiprocessing 使用注意事项。


一、为什么需要它们?

想象你是一个餐厅老板:

  • 单任务:只请一个服务员,点菜、上菜、结账全包,客人多了就忙不过来。
  • 并发:请多个服务员(线程)或开多个分店(进程),同时服务多桌客人。
  • 协程:一个服务员同时照顾多桌,等菜的间隙去点单,来回切换,效率极高。

这就是并发编程的三种基本模型:进程线程协程


二、概念详解

1. 进程(Process)

定义:程序的一次执行实例,拥有独立的地址空间、内存、文件描述符等资源。

比喻:一家独立的餐厅(有自己的厨房、员工、库存)。

特点

  • 资源隔离,一个进程崩溃不影响其他进程。
  • 创建和切换开销大。
  • 适合 CPU 密集型任务。

Python 示例

from multiprocessing import Process

def task(name):
    print(f"进程 {name} 正在运行")

if __name__ == '__main__':
    p = Process(target=task, args=('A',))
    p.start()
    p.join()
2. 线程(Thread)

定义:进程内的一个执行单元,共享进程的资源(内存、文件等)。

比喻:餐厅里的服务员(共享厨房和库存)。

特点

  • 同一进程内的线程可以共享数据。
  • 切换开销比进程小。
  • Python 有 GIL(全局解释器锁),多线程无法并行计算,但仍适用于 I/O 密集型任务。

Python 示例

from threading import Thread

def task(name):
    print(f"线程 {name} 正在运行")

t = Thread(target=task, args=('B',))
t.start()
t.join()
3. 协程(Coroutine)

定义:用户态的轻量级线程,由程序员控制调度(而非操作系统)。

比喻:一个服务员同时处理多桌客人(等菜时去点单,来回切换)。

特点

  • 单线程内并发,无需锁。
  • 切换开销极小(只保存少量上下文)。
  • 适合大量 I/O 等待场景(如网络请求、数据库查询、LLM 调用)。

Python 示例

import asyncio

async def task(name):
    print(f"协程 {name} 开始")
    await asyncio.sleep(1)
    print(f"协程 {name} 结束")

async def main():
    await asyncio.gather(task('A'), task('B'))

asyncio.run(main())

三、三者对比表

维度 进程 线程 协程
资源占用 最大(独立内存) 中等(共享内存) 最小(几 KB)
切换开销 大(系统调用) 中等(系统调用) 极小(用户态)
数据共享 复杂(IPC) 简单(需加锁) 简单(无锁)
并行能力 支持多核 Python 受 GIL 限制 单线程并发,非并行
适用场景 CPU 密集型 I/O 密集型 高并发 I/O
Python 实现 multiprocessing threading asyncio

四、进阶概念

GIL(全局解释器锁)
  • Python 特有的机制,同一时刻只允许一个线程执行 Python 字节码。
  • 导致多线程无法利用多核 CPU。
  • 解决方案:使用多进程,或使用 C 扩展(如 NumPy)。
同步 vs 异步
  • 同步:任务按顺序执行,一个完成才执行下一个。
  • 异步:任务不等待,通过回调或 await 让出控制权。
阻塞 vs 非阻塞
  • 阻塞:调用结果返回前,线程被挂起。
  • 非阻塞:立即返回,通过轮询或回调获取结果。
并发 vs 并行
  • 并发:逻辑上同时处理多件事(单核也可实现,如协程)。
  • 并行:物理上同时执行(需要多核或多机)。
Future / Task
  • 代表异步操作的“凭证”,可以 await 或添加回调。

五、实战:如何选择?

场景 推荐方案
计算圆周率、图像处理(CPU 密集) 多进程
下载多个网页、读写文件(I/O 密集) 多线程 或 协程
高并发 Web 服务(如 FastAPI) 协程(async/await
与操作系统、C 库交互 多线程
AI 项目中多次调用 LLM API 协程(asyncio

DeepAgents 项目中,大量使用 async/await 和协程,因为大部分时间都在等待 LLM 响应或网络请求,协程能最大化资源利用率。


六、一句话总结

  • 进程:重资产,隔离好,适合计算。
  • 线程:中资产,共享好,适合 I/O(Python 中注意 GIL)。
  • 协程:轻资产,切换快,适合高并发 I/O。

理解这三者,是掌握 Python 并发编程的基石。希望这篇文章能帮你彻底理清它们。


七、实战:如何查看进程信息?

在开发和调试并发程序时,了解当前进程的 ID、父进程、内存占用等信息非常有用。以下是几种常用方法:

1. 操作系统任务管理器
  • Windows:按 Ctrl + Shift + Esc 打开任务管理器 → 详细信息 → 可看到每个进程的 PID、内存、CPU 占用。
  • Linux/macOS:使用 tophtop 命令查看。
2. Python 内置方法:os.getpid()
import os
print(f"当前进程 PID: {os.getpid()}")
  • 通用(Windows / Linux / macOS),可以获取当前进程的 ID。
3. 获取父进程 PID:os.getppid() ⚠️ 平台限制
import os
if hasattr(os, 'getppid'):
    print(f"父进程 PID: {os.getppid()}")
else:
    print("当前系统不支持 os.getppid()")
  • 在 Linux / macOS 上os.getppid() 有效,返回父进程的 PID。
  • 在 Windows 上os.getppid() 不存在(会抛出 AttributeError),因为 Windows 的进程模型与 Unix 不同。
  • 跨平台替代方案:使用 psutil 库(需安装:pip install psutil)。
4. 跨平台方案:psutil
import psutil
import os

pid = os.getpid()
process = psutil.Process(pid)
print(f"当前进程 PID: {pid}")
print(f"父进程 PID: {process.ppid()}")
print(f"内存占用: {process.memory_info().rss / 1024 / 1024:.2f} MB")
print(f"CPU 使用率: {process.cpu_percent(interval=0.1)}%")

psutil 功能强大,可以获取进程的详细信息,且跨平台兼容性好。

5. 多进程模块获取当前进程信息
from multiprocessing import current_process
print(f"进程名: {current_process().name}")
print(f"进程 PID: {current_process().pid}")

总结:日常调试时,任务管理器最直观;代码中若只需当前 PID,用 os.getpid();需要父进程或更多信息,推荐使用 psutil(记得处理安装依赖)。


八、特别注意:multiprocessing 必须加 if __name__ == '__main__' 保护

在使用 multiprocessing.Process 创建新进程时,必须将启动代码放在 if __name__ == '__main__': 块内,否则在 Windows 上会引发无限递归错误。

原因:
  • Windows 没有 fork() 系统调用,创建新进程时会重新导入当前模块(即重新执行模块级别的代码)。
  • 如果不加保护,新进程在导入时会再次执行 Process(...).start(),导致无限创建子进程,最终报错 RuntimeErrorAttributeError
  • Linux / macOS 默认使用 fork(),子进程会复制父进程的内存状态,通常不会出问题,但为了跨平台兼容性,也建议加上
正确示例:
from multiprocessing import Process

def worker():
    print("子进程工作")

if __name__ == '__main__':
    p = Process(target=worker)
    p.start()
    p.join()
错误示例(不加保护):
from multiprocessing import Process

def worker():
    print("子进程工作")

# 没有 if __name__ == '__main__' 保护
p = Process(target=worker)
p.start()
p.join()

在 Windows 上运行上述代码会报错:

RuntimeError: 
        An attempt has been made to start a new process before the
        current process has finished its bootstrapping phase.
为什么线程和协程不需要?
  • 线程:线程共享进程内存空间,不会重新导入模块,因此无需此保护。
  • 协程:在单线程内并发,不涉及新进程,也无需保护。
总结:凡是用 multiprocessing 创建进程的代码,一律将入口逻辑放在 if __name__ == '__main__': 内部,这是安全且跨平台的最佳实践。

额外补充:什么是fork()?

fork() 是一个操作系统系统调用,用于创建一个新进程(子进程),这个子进程几乎是父进程的完整副本。

核心作用
  • 调用一次 fork(),但返回两次:在父进程中返回子进程的 PID,在子进程中返回 0。
  • 子进程会复制父进程的内存空间、文件描述符、环境变量等,但拥有独立的进程 ID。
简单示例(C 语言,但逻辑通用)
#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        printf("我是子进程,PID=%d\n", getpid());
    } else {
        printf("我是父进程,子进程PID=%d\n", pid);
    }
    return 0;
}
特点
  • 写时拷贝(Copy-on-Write, COW):现代 fork() 不会立即复制全部内存,而是让父子进程共享同一份物理内存,只有当一方试图修改时才会复制,节省了资源和时间。
  • 继承关系:子进程的父进程 ID(PPID)就是原父进程的 PID。
  • 独立运行:父子进程此后各自独立运行,互不干扰。
在 Python 中(multiprocessing 模块)

在 Linux/macOS 上,multiprocessing.Process 的默认启动方式就是 fork()(也可以选择 spawnforkserver)。这解释了为什么在 Linux 上创建新进程通常不需要 if __name__ == '__main__' 保护,因为子进程直接复制了父进程的状态,不会重新导入模块。

Windows 的差异

Windows 没有 fork() 系统调用。Python 的 multiprocessing 在 Windows 上使用 spawn 方式创建进程:启动一个新的 Python 解释器,重新导入模块,然后执行目标函数。这就是为什么 Windows 上必须使用 if __name__ == '__main__' 来防止无限递归创建进程。

总结
  • fork():Unix/Linux/macOS 下的进程创建方式,高效且继承了父进程状态。
  • spawn():Windows 下的替代方案,更慢但更安全,需要模块级别的保护。
  • Python 跨平台建议:永远将多进程入口放在 if __name__ == '__main__' 中,以保证在所有系统上都能正确运行。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐