核心概念速览
并发 vs 并行
通俗例子:
- 不支持并发/并行: 吃完饭才接电话
- 并发: 停下吃饭去接电话,接完继续吃 (快速切换)
- 并行: 一边吃饭一边打电话 (同时进行)
本质区别:
- 并发: 有处理多个任务的能力,但不一定同时 (单核 CPU 时间片轮转)
- 并行: 能同时处理多个任务 (多核 CPU 真正并行)
同步 vs 异步
- 同步: 等待操作完成才继续 (如:等快递送到才出门)
- 异步: 不等待,继续执行,完成后通知 (如:快递到了发短信通知)
阻塞 vs 非阻塞
- 阻塞: 调用时被操作系统挂起,必须等待
- 非阻塞: 调用后立即返回,不会被挂起
进程 (Process)
定义: 资源分配的最小单位,程序运行的实例。
特点:
- 独立的内存空间、文件描述符、系统资源
- 进程间相互独立,互不干扰
- 切换开销大 (需要保存/恢复完整上下文)
举例: 聊 QQ 和看视频是两个独立的进程
线程 (Thread)
定义: 程序执行的最小单位,也称”微进程”。
特点:
- 共享所属进程的资源 (内存、文件等)
- 切换开销较小 (共享资源,只需切换少量上下文)
- 存在资源竞争问题 (如 Python 的 GIL 锁)
举例: 播放器中,视频画面、声音输出、进度条拖动是不同的线程,它们协同工作让体验流畅。
协程 (Coroutine)
定义: 用户态的轻量级线程,程序自己控制切换。
优点:
- ✅ 极低的切换开销 (用户态切换,无需系统调用)
- ✅ 高并发支持 (单核可支持上万协程)
- ✅ 无需锁机制 (单线程内切换)
缺点:
- ❌ 无法利用多核 (本质是单线程)
- ❌ 阻塞操作会阻塞整个程序
适用场景: I/O 密集型任务 (网络请求、文件读写)
三者对比
| 对比维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 定位 | 资源分配单位 | 执行调度单位 | 用户态轻量级线程 |
| 调度者 | 操作系统 | 操作系统 | 程序自身 |
| 资源隔离 | ✅ 独立内存空间 | ❌ 共享进程资源 | ❌ 共享线程资源 |
| 并行能力 | ✅ 真并行 (多核) | ⚠️ 受 GIL 限制 | ❌ 单线程内切换 |
| 切换代价 | 高 (内核态) | 中 (系统调用) | 极低 (用户态) |
| 适用场景 | CPU 密集型 | I/O 密集型 | 高并发 I/O |
形象比喻:工厂模型
- 程序 = 设计图纸
- 进程 = 独立工厂 (有自己的土地、机器、电力)
- 线程 = 工厂里的工人 (共享工厂资源)
- GIL (Python) = 工厂机器室的唯一钥匙 (同一时刻只有一个工人能操作)
- 协程 = 聪明的工人 (等待烤箱时主动去做其他事,烤好了再回来)
最佳实践
为更直观地理解 Python 中的进程、线程与协程,使用“工厂”比喻:
设计图 → 程序(Program)
程序就像工厂的设计蓝图,尚未运行时只是计划。工厂 → 进程(Process)
蓝图投入运行后,操作系统会分配土地(内存)、机器(CPU 时间片)、接通电源(系统资源),蓝图变成一座真实的工厂。每座工厂独立,互不干扰。
• 进程 = 被系统赋予资源后、真正“开工”的程序实例。工人 → 线程(Thread)
工厂需要工人来操作机器,工人即线程。工人共享工厂资源,各自负责不同环节(主工人负责调度、记录,其他工人负责运输、组装等)。
• 线程 = 在同一工厂中共享资源的执行单元。钥匙(GIL)→ Python 的执行控制机制
Python 工厂的机器室只有一把钥匙(GIL)。每个工人操作机器前必须拿到钥匙,任意时刻只有一个工人能工作,完成后交回钥匙。这样防止资源竞争。
• GIL = 保证同一时间只有一个工人能操作机器(互斥执行)。厂区调度系统 → 操作系统调度器
工业区(操作系统)同时管理多座工厂(进程),决定谁先用电、谁暂停、何时切换(如 CFS)。
• 调度进程的是操作系统;线程的调度受操作系统 + GIL 影响。工人自我调度 → 协程(Coroutine)
有些工人会自我管理,主动切换任务以提高效率(例如:烘焙时把面团放进烤箱后去揉下一份,烤箱响再回来取)。
• 协程 = 工人自己决定何时切换任务,切换轻量且非抢占式。对比小表
| 概念 | 类比 | 调度者 | 是否并行 | 是否共享资源 | 切换代价 |
|---|---|---|---|---|---|
| 进程 | 工厂 | 操作系统 | ✅ 真并行(多核) | ❌ 否 | 高(内核级) |
| 线程 | 工人 | 操作系统 + GIL | ⚠️ 伪并行(同一时刻一人) | ✅ 是 | 中(系统级) |
| 协程 | 自我调度的工人 | 工人自身 | ❌ 并发(非并行) | ✅ 是 | 极低(用户态) |
完整故事(简述)
一份蓝图(程序)被批准投产后成为工厂(进程)。工厂雇来工人(线程),但机器室只有一把钥匙(GIL),工人们轮流使用机器。工业区调度中心(操作系统)决定哪座工厂先用电。聪明的工人(协程)会在等待 I/O 时切换任务,大幅提高效率。一句话总结
进程 = 工厂(资源容器);线程 = 工人(执行单元);GIL = 唯一钥匙(互斥);操作系统 = 厂区调度;协程 = 自主切换的聪明工人。
(可按需将此节放在 最佳实践 之后或作为附录)
最佳实践
场景选择
| 任务类型 | 推荐方案 | 原因 |
|---|---|---|
| CPU 密集型 | 多进程 | 绕开 GIL,充分利用多核 |
| I/O 密集型 | 多线程/协程 | 等待 I/O 时可切换到其他任务 |
| 高并发 I/O | 协程 | 极低的切换开销,支持海量并发 |
| 混合场景 | 多进程 + 协程 | 进程利用多核,协程提升单核效率 |
代码示例
CPU 密集型:使用多进程
1 | from multiprocessing import Pool |
I/O 密集型:使用协程
1 | import asyncio |
并发数量控制
光有”多进程/多线程/多协程”还不够,还要控制”同时跑多少个”,否则资源会被打爆。
进程数 — multiprocessing.Pool(processes=N):
1 | import os |
经验值:N == cpu_count() (CPU 密集)。再多不会更快,反而增加调度开销。
线程数 — ThreadPoolExecutor(max_workers=N):
1 | from concurrent.futures import ThreadPoolExecutor |
经验值:I/O 密集 32~128 (按下游承载调整);CPU 密集受 GIL 限制,线程数意义不大。需精细限流时用 Semaphore:
1 | import threading |
协程并发 — asyncio.Semaphore 限流 + asyncio.gather 并发启动:
1 | import asyncio |
注意:不加 Semaphore 时 gather 会瞬间发起全部任务,可能击穿下游连接池、文件句柄、内存上限。
总览对比
| 维度 | 进程数 | 线程数 | 协程并发 |
|---|---|---|---|
| 控制入口 | Pool(processes=) |
ThreadPoolExecutor(max_workers=) / Semaphore |
asyncio.Semaphore |
| 默认值 | cpu_count() |
min(32, cpu_count()+4) |
无上限(必须自己控) |
| 经验值 | cpu_count() |
I/O: 32~128 | I/O: 10~100 (看下游承载) |
线程同步
线程共享进程资源,会有竞争 (全局变量、文件、连接)。锁保护临界区。
Lock — 同一时刻只允许一个线程进入:
1 | import threading |
无锁时 counter += 1 的”读-改-写”会被打断,结果小于 100。
其他原语 (按需选用):
RLock:可重入,同一线程可多次acquireCondition:等待某条件成立 (生产者-消费者)Event:线程间发信号 (一端set(),另一端wait())Semaphore:限流 (上节)
死锁 — 警惕循环等待:
1 | # A 等 B,B 等 A → 永久阻塞 |
解法:统一加锁顺序,或用 acquire(timeout=)。
进程间通信 (IPC)
进程内存相互隔离,数据靠 IPC 传递。
| 方式 | 适用 | 特点 |
|---|---|---|
Queue / Pipe |
消息传递 | 安全、跨平台;Pipe 仅 2 进程 |
Manager() |
共享 Python 对象 | 列表/字典/锁,基于代理,有开销 |
Value / Array (共享内存) |
大块数据 | 零拷贝,需自己加锁 |
最常用 — Queue:
1 | from multiprocessing import Process, Queue |
注意:multiprocessing.Queue 内部已加锁,跨进程安全;不要和 queue.Queue (线程版) 混用。
常见误区
1. GIL 不解决一切 — Python 3.12 及之前 GIL 仍在,CPU 密集用线程 ≈ 单线程。CPU 密集一律上多进程。Python 3.13 起可启用 free-threaded mode (PEP 703),关掉 GIL,但仍标 experimental,见下节。
2. asyncio ≠ 自动加速 — 它是 单线程 调度,加速来自”等 IO 时做别的事”。阻塞调用会卡死整个事件循环:
1 | async def bad(): |
3. 并发不是越多越好 — 1000 个并发 HTTP 请求打到 100 并发的服务,后端排队、连接池打爆、超时雪崩。要按 下游承载 限流 (上节 Semaphore)。
4. daemon=True ≠ 后台运行 — 只表示主线程退出时它被强制终止,不保证”在后台安静跑”。
5. 进程池 ≠ 线程池 — multiprocessing.Pool 不能放不可 pickle 的对象 (lambda、嵌套函数、文件句柄、数据库连接)。线程池无此限制。
GIL 现状与 free-threaded mode (PEP 703)
GIL 是什么 — CPython 解释器的全局锁,保证同一时刻只有一个线程执行 Python 字节码。设计目的是保护 C 扩展的内存安全,代价是 CPU 密集型多线程 ≈ 单线程。
PEP 703 的目标 — 让 GIL 可关闭,线程真正并行于多核。由 Meta 出人主导,Python 3.13 起 实验性 支持 (3.14 仍在优化,未默认开启)。
怎么用 — 装一个专门的 free-threaded 解释器:
1 | # pyenv 编译时关 GIL |
可执行文件末尾带 t (python3.13t),sys.abiflags 含 't'。普通 python3.13 默认仍带 GIL,两者 ABI 不兼容。
运行时检测:
1 | import sysconfig |
已知代价:
- 单线程性能回退 — 引用计数改用原子操作 + 偏向锁,单线程开销高于普通构建 (具体数字视负载,官方文档只说”有开销”,社区基准常见 5~40%)
- C 扩展必须声明 — 未声明的扩展会被自动加回 GIL,极端情况整个解释器退化为带 GIL 模式。声明方式:主流库 (numpy、pandas、scikit-learn、aiohttp、requests) 在 3.13/3.14 周期陆续适配,但老旧 C 扩展可能没跟上
1
2
3
4
5
6static PyModuleDef_Slot my_slots[] = {
{Py_mod_gil, Py_MOD_GIL_NOT_USED}, // "我不依赖 GIL"
{0, NULL}
}; - ABI 不通用 — 必须用对应的
python3.13t -m pip装包:1
python3.13t -m pip install numpy # free-threaded 版
实战建议:
- 生产环境暂缓 — 仍 experimental,先看 cpython 官方兼容性表
- CPU 密集纯 Python 代码 — free-threaded 多线程可能受益,但 先 benchmark,别无脑上
- I/O 密集 — 仍首选协程,free-threaded 帮不了等 IO
- 新项目 — 可以试;老项目升级前先查依赖兼容矩阵
怎么选?决策树
1 | 你的任务是什么? |
一句话:CPU 多进程、IO 协程优先,线程是兜底。
关键原则
- CPU 密集 → 多进程 (计算、循环、数据处理)
- I/O 密集 → 协程 > 线程 (网络、文件、数据库)
- 高性能 → 多进程 + 协程 (充分利用硬件 + 高效调度)