Python 并发怎么选?一篇讲清 threading、multiprocessing、asyncio 的边界

发布时间:2026/10/1 8:36:39

Python 并发怎么选?一篇讲清 threading、multiprocessing、asyncio 的边界 写过一些 Python 服务和脚本后我发现很多人包括早期的我一遇到要并发就凭感觉开线程请求慢了开线程、算得慢了也开线程跑起来才发现IO 任务确实快了可 CPU 密集的任务开了一堆线程反而更慢。问题不在并发本身而在没搞清楚三种模型各自解决什么问题。这篇把threading、multiprocessing、asyncio的边界、适用场景和高频踩坑一次讲清代码都可直接运行。一、前提GIL 决定了线程能做什么、不能做什么CPython 有一把全局解释器锁GIL同一个进程里同一时刻只有一个线程在执行 Python 字节码。这句话是后面所有选型的基础。对CPU 密集型任务大量纯 Python 计算多线程在 CPython 里无法真正并行多个线程轮流拿 GIL还要付切换开销结果往往是开了多线程速度不升反降。对IO 密集型任务网络请求、读写文件、查数据库线程在等待 IO 时会释放 GIL这段等待时间可以让给别的线程所以多线程、协程都能拿到明显的并发收益。想在 CPU 密集场景利用多核正统办法是多进程每个进程有独立解释器和 GIL另外 NumPy 这类 C 扩展在做底层计算时会主动释放 GIL属于特例。所以选型的第一刀永远是先判断任务到底是CPU 密集还是IO 密集。二、三种模型分别是什么1. threading线程配合同步阻塞式 API线程共享同一块内存上手最简单。实际工程里一般不直接Thread().start()而是用线程池import concurrent.futures as cf import time def fetch(url): time.sleep(1) # 模拟一次 1 秒的网络往返阻塞、会释放 GIL return fdone: {url} urls [fhttps://example.com/{i} for i in range(10)] start time.time() with cf.ThreadPoolExecutor(max_workers10) as pool: results list(pool.map(fetch, urls)) print(round(time.time() - start, 2)) # 约 1.0 秒而不是串行的 10 秒适合用的是requests、传统数据库驱动这类同步阻塞库任务以等待 IO 为主。注意有 GIL 不等于不用加锁。GIL 只保证单条字节码的原子性而counter 1在字节码层面是读值→相加→写回三步线程可能在中间被切换counter 0 def inc(): global counter for _ in range(100_000): counter 1 # 非原子操作多线程下最终值会小于预期凡是先读后写的复合操作、对共享可变状态的修改都要显式用threading.Lock保护。2. multiprocessing进程绕开 GIL 吃满多核多进程各自有独立的解释器和内存空间能真正跑在多个 CPU 核心上是 CPU 密集任务的标准答案import concurrent.futures as cf import math, os, time def heavy(n): # 纯 Python 的 CPU 密集计算 return sum(math.factorial(i) % 7919 for i in range(n)) if __name__ __main__: tasks [30_000] * os.cpu_count() start time.time() with cf.ProcessPoolExecutor() as pool: list(pool.map(heavy, tasks)) print(round(time.time() - start, 2), s)同样的heavy换成ThreadPoolExecutor你会发现耗时基本不变甚至更长——这就是 GIL 的直观体现。多进程的代价也要清楚进程启动比线程重任务太小、数量太多时创建和调度开销会盖过收益。进程间内存不共享传给子进程的参数和返回值都要能被pickle序列化跨进程传大对象很慢。跨平台尤其 Windows、macOS 的 spawn 启动方式必须写if __name__ __main__:保护否则子进程递归 import 主模块会反复创建进程甚至报错。3. asyncio单线程事件循环 协程扛海量 IO 连接asyncio不开多线程也不开多进程而是在一个线程里跑一个事件循环靠await在任务之间协作式切换。它特别适合连接数特别多、大部分时间都在等的场景比如爬虫、网关、长连接import asyncio, time async def fetch(i): await asyncio.sleep(1) # 模拟异步 IO等待时把控制权交还事件循环 return i async def main(): return await asyncio.gather(*(fetch(i) for i in range(10))) start time.time() asyncio.run(main()) print(round(time.time() - start, 2)) # 约 1.0 秒协程的切换只发生在await点没有线程切换和 GIL 争用所以单线程就能轻松挂起成百上千个等待中的任务。但它有一个极其关键的前提整条调用链都得是异步的。三、最容易踩的坑在协程里写阻塞代码协程是协作式调度事件循环本身只有一个线程。一旦某个协程里出现同步阻塞调用整个循环都会被卡住所有并发瞬间退化成串行import asyncio, time, requests async def bad_fetch(url): return requests.get(url, timeout5).status_code # 同步阻塞整个循环卡死 async def main(): await asyncio.gather(*(bad_fetch(u) for u in urls)) # 看起来并发实际一个个排队time.sleep、requests.get、同步的数据库驱动、任何纯 Python 的重计算都不能直接放进协程。正确做法有两种用对应的异步库aiohttp替代requests、异步数据库驱动替代同步驱动、asyncio.sleep替代time.sleep老的同步库一时换不掉就用loop.run_in_executor把阻塞函数丢进线程池事件循环继续调度别的协程import asyncio, concurrent.futures as cf import requests def get(url): return requests.get(url, timeout5).status_code async def main(urls): loop asyncio.get_running_loop() with cf.ThreadPoolExecutor(max_workers10) as pool: results await asyncio.gather( *(loop.run_in_executor(pool, get, u) for u in urls) ) return results这也是asyncio 线程池混合模型最常见的用法事件循环负责海量连接的调度少量阻塞调用外包给线程池。四、一张表做选型任务特征推荐方案关键原因大量网络/文件 IO且用的是异步库aiohttp、异步驱动asyncio单线程高并发连接数多时开销最低IO 密集但用的是同步阻塞库requests、传统驱动ThreadPoolExecutor等待 IO 时释放 GIL改造量最小CPU 密集计算、压缩、图像处理、数据加工ProcessPoolExecutor绕开 GIL真正利用多核既要海量 IO 并发又有少量同步阻塞调用asynciorun_in_executor事件循环调度阻塞活外包给线程池需要严格隔离、绕过崩溃影响多进程进程内存独立互不拖垮补充两个工程默认值Python 3.8 里ThreadPoolExecutor的max_workers默认是min(32, os.cpu_count() 4)ProcessPoolExecutor默认是os.cpu_count()。线程/进程不是越多越好还要考虑对方服务的限流、连接池大小和本机内存。五、结论我的判断顺序固定成三步先量再选用计时或 cProfile 确认瓶颈是 CPU 还是 IO别靠猜。IO 看库全链路异步就上asyncio还在用同步阻塞库就先用线程池改造最省事。CPU 上进程纯计算要吃多核就用进程池并记得__main__保护和参数可 pickle。并发不是银弹。很多时候脚本慢根因是外部接口、数据库索引或串行的重复请求而不是并发开得不够多。先定位瓶颈再按上面的边界选模型比一上来就堆线程、堆协程靠谱得多。
延伸阅读

更多相关文章

2026/10/1 8:31:39

四、快速排序算法讲解

本文是《排序算法系列》第四篇。前三篇我们走过了 O(n) 家族(冒泡、插入、选择)、分治代表(归并)、以及两个"进化型"算法(希尔、堆)。这一篇我们聚焦快速排序——实际应用最广泛、平均性能最好的…

2026/10/1 9:21:41

基于YOLO的手势检测应用设计:从训练到部署全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 9:21:41

USDT地址怎么选?Omni、ERC20、TRC20三种类型对比与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 9:21:41

AI工程化从零搭建:契约驱动的可交付AI服务实践

1. 这不是调包,是亲手搭起AI工程的骨架“AI Engineering from Scratch”——看到这个标题,很多人第一反应是:又要从零写Transformer?不,完全不是。我带过六支AI落地团队,经手过金融风控、工业质检、医疗影像…

2026/10/1 9:21:41

Unity与UE5本质不是选择题,而是项目约束求解器

1. 这不是“选哪个更好”的选择题,而是“你正在解决什么问题”的诊断书 Unity和UE5对比,这个标题在开发者社区里刷屏了十年,但绝大多数讨论都卡在“谁更强大”“谁更适合新手”“谁美术效果更炸”这种表层判断上。我从2013年用Unity 4.3做第一…

2026/10/1 9:21:41

改进YOLOv8的电力设备缺陷分割实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 9:16:41

TestStand本地化实战:从系统界面到操作员界面的多语言改造

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/1 5:21:14

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/9/29 21:48:03

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 7:00:49

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑