【python学习】进程、线程、协程是什么
📝 进程、线程、协程:从零搞懂并发三兄弟(附实战补充)
在学习 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:使用
top或htop命令查看。
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(),导致无限创建子进程,最终报错RuntimeError或AttributeError。 - 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()(也可以选择 spawn 或 forkserver)。这解释了为什么在 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__'中,以保证在所有系统上都能正确运行。
更多推荐


所有评论(0)