应根据I/O类型、CPU占用等动态选择threading/multiprocessing/asyncio:asyncio适用于高并发网络I/O,threading适合胶水集成但线程数不宜超100,multiprocessing需注意Windows spawn与Linux fork差异。

Python 多任务编程不是“选一个就完事”,而是根据 I/O 类型、CPU 占用、并发规模和错误容忍度,动态匹配 threading、multiprocessing 或 asyncio —— 用错模型,轻则性能不升反降,重则死锁、资源泄漏、结果错乱。
什么时候该用 asyncio 而不是 threading
本质区别不在“快慢”,而在阻塞类型:当任务大量等待网络响应(HTTP 请求、数据库查询、Redis 操作)或文件读写(非 mmap 场景),且单机需支撑数千并发时,asyncio 是更轻量的选择;threading 在这种场景下反而因 GIL 和线程切换开销成为瓶颈。
-
asyncio要求所有 I/O 调用必须是异步的(如aiohttp替代requests,aiomysql替代pymysql),混用同步阻塞调用(比如在async def里直接调time.sleep(1))会卡住整个事件循环 -
threading可以无缝调用任意同步库,适合胶水式集成,但线程数超过 100 后,内存占用和上下文切换成本陡增 - 常见误判:把“有多个 API 调用”直接等同于“适合 asyncio”——如果这些 API 是本地计算密集型(如图像缩放、加密解密),
asyncio不仅无益,还会因协程调度引入额外延迟
multiprocessing 启动方式在 Linux/macOS 和 Windows 上行为不同
Windows 默认使用 spawn 方式启动子进程,意味着每个子进程会重新导入主模块;Linux/macOS 默认用 fork,直接复制父进程内存镜像。这个差异直接影响代码结构和变量共享逻辑。
- Windows 下若主模块顶层有不可序列化对象(如打开的文件句柄、
threading.Lock实例),或执行了带副作用的语句(如print("init")),会导致子进程启动失败并抛出RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase. - 解决方法:始终将多进程入口包裹在
if __name__ == "__main__":下,并避免在模块顶层做复杂初始化 -
fork方式虽快,但在多线程程序中调用multiprocessing可能引发死锁(例如父进程某线程正持有锁时 fork,子进程继承锁但无对应线程释放它),此时应显式设置mp.set_start_method("spawn")
concurrent.futures 的 ThreadPoolExecutor 和 ProcessPoolExecutor 怎么选参数
最大线程/进程数不是越大越好,要结合硬件与任务特征反推:线程池大小通常设为 min(32, os.cpu_count() + 4) 是较稳妥的起点;进程池大小一般不超过 os.cpu_count(),除非任务存在长时间 I/O 等待且 CPU 利用率持续低于 30%。
立即学习“Python免费学习笔记(深入)”;
- 对纯 CPU 密集型任务(如数值计算、文本解析),
ProcessPoolExecutor才能真正并行;ThreadPoolExecutor受限于 GIL,多个线程实际串行执行 - 对高并发 I/O 任务(如爬取 500 个网页),
ThreadPoolExecutor(max_workers=20)往往比max_workers=100更稳定——太多线程会加剧系统调度压力,还可能触发目标站点限流或连接拒绝 - 注意
submit()返回的Future对象本身不阻塞,但调用.result()会;批量提交后统一as_completed()获取结果,比逐个.result()更高效
真正难的不是写出来,而是判断哪个任务该走哪条路——比如一个 Web 服务既要实时处理 WebSocket 消息(适合 asyncio),又要定时跑报表导出(CPU 密集,适合 multiprocessing),还得调用遗留的同步 SDK(只能塞进线程池)。这时候混合模型不是炫技,而是刚需;但混合也意味着你得亲手管理事件循环、进程生命周期和线程安全边界,稍不留神,RuntimeError: Event loop is closed 或 BrokenPipeError 就在等你。



















