核心概念速览

并发 vs 并行

通俗例子:

  • 不支持并发/并行: 吃完饭才接电话
  • 并发: 停下吃饭去接电话,接完继续吃 (快速切换)
  • 并行: 一边吃饭一边打电话 (同时进行)

本质区别:

  • 并发: 有处理多个任务的能力,但不一定同时 (单核 CPU 时间片轮转)
  • 并行: 能同时处理多个任务 (多核 CPU 真正并行)

同步 vs 异步

  • 同步: 等待操作完成才继续 (如:等快递送到才出门)
  • 异步: 不等待,继续执行,完成后通知 (如:快递到了发短信通知)

阻塞 vs 非阻塞

  • 阻塞: 调用时被操作系统挂起,必须等待
  • 非阻塞: 调用后立即返回,不会被挂起

进程 (Process)

定义: 资源分配的最小单位,程序运行的实例。

特点:

  • 独立的内存空间、文件描述符、系统资源
  • 进程间相互独立,互不干扰
  • 切换开销大 (需要保存/恢复完整上下文)

举例: 聊 QQ 和看视频是两个独立的进程

线程 (Thread)

定义: 程序执行的最小单位,也称”微进程”。

特点:

  • 共享所属进程的资源 (内存、文件等)
  • 切换开销较小 (共享资源,只需切换少量上下文)
  • 存在资源竞争问题 (如 Python 的 GIL 锁)

举例: 播放器中,视频画面、声音输出、进度条拖动是不同的线程,它们协同工作让体验流畅。

协程 (Coroutine)

定义: 用户态的轻量级线程,程序自己控制切换。

优点:

  • ✅ 极低的切换开销 (用户态切换,无需系统调用)
  • ✅ 高并发支持 (单核可支持上万协程)
  • ✅ 无需锁机制 (单线程内切换)

缺点:

  • ❌ 无法利用多核 (本质是单线程)
  • ❌ 阻塞操作会阻塞整个程序

适用场景: I/O 密集型任务 (网络请求、文件读写)


三者对比

对比维度 进程 线程 协程
定位 资源分配单位 执行调度单位 用户态轻量级线程
调度者 操作系统 操作系统 程序自身
资源隔离 ✅ 独立内存空间 ❌ 共享进程资源 ❌ 共享线程资源
并行能力 ✅ 真并行 (多核) ⚠️ 受 GIL 限制 ❌ 单线程内切换
切换代价 高 (内核态) 中 (系统调用) 极低 (用户态)
适用场景 CPU 密集型 I/O 密集型 高并发 I/O

形象比喻:工厂模型

  • 程序 = 设计图纸
  • 进程 = 独立工厂 (有自己的土地、机器、电力)
  • 线程 = 工厂里的工人 (共享工厂资源)
  • GIL (Python) = 工厂机器室的唯一钥匙 (同一时刻只有一个工人能操作)
  • 协程 = 聪明的工人 (等待烤箱时主动去做其他事,烤好了再回来)

最佳实践

为更直观地理解 Python 中的进程、线程与协程,使用“工厂”比喻:

  1. 设计图 → 程序(Program)
    程序就像工厂的设计蓝图,尚未运行时只是计划。

  2. 工厂 → 进程(Process)
    蓝图投入运行后,操作系统会分配土地(内存)、机器(CPU 时间片)、接通电源(系统资源),蓝图变成一座真实的工厂。每座工厂独立,互不干扰。
    • 进程 = 被系统赋予资源后、真正“开工”的程序实例。

  3. 工人 → 线程(Thread)
    工厂需要工人来操作机器,工人即线程。工人共享工厂资源,各自负责不同环节(主工人负责调度、记录,其他工人负责运输、组装等)。
    • 线程 = 在同一工厂中共享资源的执行单元。

  4. 钥匙(GIL)→ Python 的执行控制机制
    Python 工厂的机器室只有一把钥匙(GIL)。每个工人操作机器前必须拿到钥匙,任意时刻只有一个工人能工作,完成后交回钥匙。这样防止资源竞争。
    • GIL = 保证同一时间只有一个工人能操作机器(互斥执行)。

  5. 厂区调度系统 → 操作系统调度器
    工业区(操作系统)同时管理多座工厂(进程),决定谁先用电、谁暂停、何时切换(如 CFS)。
    • 调度进程的是操作系统;线程的调度受操作系统 + GIL 影响。

  6. 工人自我调度 → 协程(Coroutine)
    有些工人会自我管理,主动切换任务以提高效率(例如:烘焙时把面团放进烤箱后去揉下一份,烤箱响再回来取)。
    • 协程 = 工人自己决定何时切换任务,切换轻量且非抢占式。

  7. 对比小表

概念 类比 调度者 是否并行 是否共享资源 切换代价
进程 工厂 操作系统 ✅ 真并行(多核) ❌ 否 高(内核级)
线程 工人 操作系统 + GIL ⚠️ 伪并行(同一时刻一人) ✅ 是 中(系统级)
协程 自我调度的工人 工人自身 ❌ 并发(非并行) ✅ 是 极低(用户态)
  1. 完整故事(简述)
    一份蓝图(程序)被批准投产后成为工厂(进程)。工厂雇来工人(线程),但机器室只有一把钥匙(GIL),工人们轮流使用机器。工业区调度中心(操作系统)决定哪座工厂先用电。聪明的工人(协程)会在等待 I/O 时切换任务,大幅提高效率。

  2. 一句话总结
    进程 = 工厂(资源容器);线程 = 工人(执行单元);GIL = 唯一钥匙(互斥);操作系统 = 厂区调度;协程 = 自主切换的聪明工人。

(可按需将此节放在 最佳实践 之后或作为附录)

最佳实践

场景选择

任务类型 推荐方案 原因
CPU 密集型 多进程 绕开 GIL,充分利用多核
I/O 密集型 多线程/协程 等待 I/O 时可切换到其他任务
高并发 I/O 协程 极低的切换开销,支持海量并发
混合场景 多进程 + 协程 进程利用多核,协程提升单核效率

代码示例

CPU 密集型:使用多进程

1
2
3
4
5
6
7
from multiprocessing import Pool

def cpu_bound_task(n):
return sum(i * i for i in range(n))

with Pool(4) as p:
results = p.map(cpu_bound_task, [10**7] * 4)

I/O 密集型:使用协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio
import aiohttp

async def fetch_url(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()

async def main():
urls = ['http://example.com'] * 100
tasks = [fetch_url(url) for url in urls]
return await asyncio.gather(*tasks)

asyncio.run(main())

并发数量控制

光有”多进程/多线程/多协程”还不够,还要控制”同时跑多少个”,否则资源会被打爆。

进程数multiprocessing.Pool(processes=N)

1
2
3
4
5
6
import os
from multiprocessing import Pool

N = os.cpu_count() # 与核数对齐
with Pool(processes=N) as p:
p.map(cpu_bound_task, jobs)

经验值:N == cpu_count() (CPU 密集)。再多不会更快,反而增加调度开销。

线程数ThreadPoolExecutor(max_workers=N):

1
2
3
4
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=32) as ex:
ex.map(io_task, urls)

经验值:I/O 密集 32~128 (按下游承载调整);CPU 密集受 GIL 限制,线程数意义不大。需精细限流时用 Semaphore:

1
2
3
4
5
6
import threading
sem = threading.Semaphore(8)

def job():
with sem:
do_io()

协程并发asyncio.Semaphore 限流 + asyncio.gather 并发启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
import asyncio
import aiohttp

sem = asyncio.Semaphore(10)

async def fetch(url):
async with sem: # 同时最多 10 个请求
async with aiohttp.ClientSession() as session:
return await session.get(url)

async def main():
tasks = [fetch(u) for u in urls]
return await asyncio.gather(*tasks) # 一次性启动,通过 Semaphore 限流

注意:不加 Semaphoregather 会瞬间发起全部任务,可能击穿下游连接池、文件句柄、内存上限。

总览对比

维度 进程数 线程数 协程并发
控制入口 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
2
3
4
5
6
7
8
9
10
11
12
13
import threading

counter = 0
lock = threading.Lock()

def inc():
global counter
with lock: # 自动获取/释放
counter += 1

threads = [threading.Thread(target=inc) for _ in range(100)]
for t in threads: t.start()
for t in threads: t.join()

无锁时 counter += 1 的”读-改-写”会被打断,结果小于 100。

其他原语 (按需选用):

  • RLock:可重入,同一线程可多次 acquire
  • Condition:等待某条件成立 (生产者-消费者)
  • Event:线程间发信号 (一端 set(),另一端 wait())
  • Semaphore:限流 (上节)

死锁 — 警惕循环等待:

1
2
3
# A 等 B,B 等 A → 永久阻塞
lock_a.acquire(); lock_b.acquire() # 线程 1
lock_b.acquire(); lock_a.acquire() # 线程 2

解法:统一加锁顺序,或用 acquire(timeout=)

进程间通信 (IPC)

进程内存相互隔离,数据靠 IPC 传递。

方式 适用 特点
Queue / Pipe 消息传递 安全、跨平台;Pipe 仅 2 进程
Manager() 共享 Python 对象 列表/字典/锁,基于代理,有开销
Value / Array (共享内存) 大块数据 零拷贝,需自己加锁

最常用 — Queue:

1
2
3
4
5
6
7
8
9
10
from multiprocessing import Process, Queue

def worker(q):
q.put(sum(range(10**6)))

q = Queue()
ps = [Process(target=worker, args=(q,)) for _ in range(4)]
for p in ps: p.start()
for p in ps: p.join()
results = [q.get() for _ in ps]

注意: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
2
3
4
5
async def bad():
time.sleep(1) # ❌ 阻塞整个 loop
requests.get(url) # ❌ 同步库,同样阻塞
await asyncio.sleep(1) # ✅ 让出控制权
await session.get(url) # ✅ 配合 aiohttp / httpx

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
2
3
# pyenv 编译时关 GIL
env PYTHON_CONFIGURE_OPTS="--disable-gil" pyenv install 3.13t
# 或从 python.org 下载带 free-threaded 的安装包

可执行文件末尾带 t (python3.13t),sys.abiflags't'。普通 python3.13 默认仍带 GIL,两者 ABI 不兼容。

运行时检测:

1
2
import sysconfig
is_free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED") is not None

已知代价:

  • 单线程性能回退 — 引用计数改用原子操作 + 偏向锁,单线程开销高于普通构建 (具体数字视负载,官方文档只说”有开销”,社区基准常见 5~40%)
  • C 扩展必须声明 — 未声明的扩展会被自动加回 GIL,极端情况整个解释器退化为带 GIL 模式。声明方式:
    1
    2
    3
    4
    5
    6
    static PyModuleDef_Slot my_slots[] = {
    #if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED}, // "我不依赖 GIL"
    #endif
    {0, NULL}
    };
    主流库 (numpy、pandas、scikit-learn、aiohttp、requests) 在 3.13/3.14 周期陆续适配,但老旧 C 扩展可能没跟上
  • ABI 不通用 — 必须用对应的 python3.13t -m pip 装包:
    1
    python3.13t -m pip install numpy   # free-threaded 版

实战建议:

  1. 生产环境暂缓 — 仍 experimental,先看 cpython 官方兼容性表
  2. CPU 密集纯 Python 代码 — free-threaded 多线程可能受益,但 先 benchmark,别无脑上
  3. I/O 密集 — 仍首选协程,free-threaded 帮不了等 IO
  4. 新项目 — 可以试;老项目升级前先查依赖兼容矩阵

怎么选?决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你的任务是什么?

├─ CPU 密集 (计算 / 循环 / 数据处理)
│ └─ 多进程 (multiprocessing.Pool)

└─ I/O 密集 (网络 / 文件 / 数据库)

├─ 并发量 < 几百?
│ └─ 多线程 (ThreadPoolExecutor) — 写法简单

└─ 并发量 > 几千?
└─ 协程 (asyncio + aiohttp) — 切换开销低

└─ 还要压榨多核?
└─ 多进程 + 协程 (每进程跑独立事件循环)

一句话:CPU 多进程、IO 协程优先,线程是兜底。

关键原则

  1. CPU 密集 → 多进程 (计算、循环、数据处理)
  2. I/O 密集 → 协程 > 线程 (网络、文件、数据库)
  3. 高性能 → 多进程 + 协程 (充分利用硬件 + 高效调度)