
你好,我是老李。
九块路线总览见《Python 到 FastAPI 九层进阶》。第六块解决了单条执行路径如何留下失败证据;当任务同时推进,问题会多出共享状态、取消传播和资源上限。先问瓶颈在哪里,再谈线程、进程或协程。
老李排查过一个“线程越多越慢”的抓取服务:请求很快,却把响应解析和压缩都塞在线程池里,CPU 争用与锁等待同时放大。并发不是数量按钮,而是一组资源与调度约束。
这一块的验收标准是:能依据 I/O 等待和 CPU 计算选择模型,用锁或消息传递保护状态,并为超时、取消和背压设计出口
这一块沿着“解释器约束—线程—进程—协程—协作原语—统一接口”展开。示例刻意保持结果确定,便于验证并发控制是否真实生效。
官方概念
传统 CPython 构建使用全局解释器锁协调 Python 字节码执行;Python 3.13 起提供实验性自由线程构建,但它是单独配置,并非所有安装的默认状态。
类比记忆
像办公区有一枚共享通行牌;自由线程构建尝试撤掉通行牌,却要求家具、门锁和第三方扩展都重新接受并发检查。
import sysconfig
free_threaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
print("自由线程构建" if free_threaded else "传统构建")
print(type(free_threaded).__name__)
运行输出:
传统构建
bool
使用边界
自由线程支持仍在演进,扩展模块可能声明不支持并重新启用 GIL。无论构建类型如何,复合读改写都不能凭直觉当作原子操作。

官方概念
线程共享同一进程内存,创建成本较低,适合等待网络或磁盘;共享可变状态必须用锁、队列或不可变数据明确同步。
类比记忆
像多人共用一块白板:交流快,但同时改同一格会覆盖;锁是一次只允许一人书写的白板笔。
from threading import Lock, Thread
total = 0
lock = Lock()
def add_many() -> None:
global total
for _ in range(1000):
with lock:
total += 1
threads = [Thread(target=add_many) for _ in range(4)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(total)
运行输出:
4000
使用边界
锁范围过大会串行化,多个锁顺序不一致可能死锁。CPU 密集型纯 Python 工作在传统构建中通常不因增加线程获得线性加速。

官方概念
进程拥有独立解释器和地址空间,可使用多个 CPU 核心;参数和结果通常需要序列化,通过队列、管道或共享内存通信。
类比记忆
像把工作分到不同厨房:可同时开火且互不碰撞,但食材和成品必须经过传送口,搬运也有成本。
from multiprocessing import get_context
def double(value: int, output) -> None:
output.put(value * 2)
if __name__ == "__main__":
context = get_context("fork")
output = context.Queue()
process = context.Process(target=double, args=(21, output))
process.start()
print(output.get())
process.join()
运行输出:
42
使用边界
Windows 和 macOS 常用 spawn,入口必须由 if __name__ == "__main__" 保护,可调用目标需可导入。大对象传输可能抵消并行收益。

官方概念
事件循环在协程执行到 await 时调度其他任务;TaskGroup 自 Python 3.11 提供结构化并发,退出作用域前等待任务并统一传播失败。
类比记忆
像一名厨师同时照看多口锅:水沸等待时切换到备菜,但切菜本身若耗时过长,其他锅仍无人照看。
import asyncio
async def fetch(name: str, delay: float) -> str:
await asyncio.sleep(delay)
return name
async def main() -> None:
async with asyncio.TaskGroup() as group:
first = group.create_task(fetch("A", 0.02))
second = group.create_task(fetch("B", 0.01))
print(sorted([first.result(), second.result()]))
asyncio.run(main())
运行输出:
['A', 'B']
使用边界
协程适合高并发 I/O,不会自动加速阻塞函数或 CPU 计算。同步阻塞调用需换异步驱动、交给线程,或拆到进程。

官方概念
有界队列提供背压,wait_for 为等待设置期限,取消信号让无用工作尽快退出;to_thread 可把短期无法替换的阻塞函数移出事件循环线程。
类比记忆
像有容量的候车厅:满员就暂停放行,末班时间到了便取消行程,临时人工窗口负责旧式纸质流程。
import asyncio
async def main() -> None:
queue = asyncio.Queue(maxsize=1)
await queue.put("任务")
item = await asyncio.wait_for(queue.get(), timeout=0.1)
queue.task_done()
total = await asyncio.to_thread(sum, [1, 2, 3])
print(item, total, queue.empty())
asyncio.run(main())
运行输出:
任务 6 True
使用边界
超时后要确认底层操作是否真正取消,尤其是线程中的阻塞调用。队列必须有容量、消费者退出协议和失败处置,不能只做无限缓冲。

官方概念
Executor 用 submit、map 和 Future 统一任务接口;线程池面向阻塞 I/O,进程池面向可序列化的 CPU 任务。map 按输入顺序给出结果。
类比记忆
像统一派工台:工单格式相同,后场可换成共享办公室或独立车间,但每种后场的运输和隔离成本不同。
from concurrent.futures import ThreadPoolExecutor
def normalize(name: str) -> str:
return name.strip().upper()
with ThreadPoolExecutor(max_workers=2) as executor:
results = list(executor.map(normalize, [" a", "b ", " c "]))
print(results)
运行输出:
['A', 'B', 'C']
使用边界
池大小必须服从下游连接数、内存和限流约束。Future.result() 会重新抛出任务异常;若只提交不收集,失败可能被延迟发现。

事实:线程、进程和协程都不能增加下游数据库容量。
因果链:盲目加并发 → 排队与超时增加 → 重试继续放大流量。
影响:按连接池、限流与延迟预算设置并发上限,系统更稳定。
边界:离线批处理可在隔离资源下采用不同上限。

事实:游离任务可能活过创建它的请求。
因果链:父任务结束 → 子任务继续占资源 → 异常无人接收。
影响:作用域统一等待和取消,让完成条件可证明。
边界:长期守护任务需要独立监督器而非请求作用域。

事实:共享可变状态需要同步并形成争用点。
因果链:写者增加 → 锁竞争上升 → 时序缺陷难复现。
影响:按所有者处理状态、通过消息交接,可缩小临界区。
边界:消息也需容量、幂等和失败重放策略。

并发与并行:并发让任务交错推进,并行让任务同一时刻执行;线程与进程:线程共享内存且启动轻,进程隔离内存且可使用多核;协程与线程:协程在显式等待点协作切换,线程由系统调度;超时与取消:超时是期限策略,取消是传播停止请求,底层操作未必立即终止。
并发设计先识别工作负载:等待型任务可选线程或协程,CPU 型任务评估进程或经验证的自由线程构建。共享状态必须同步,任务必须有作用域,队列必须有容量,超时之后仍要确认资源真正释放。下一块将用高级语言机制提炼这些重复控制结构。
