asyncio在高并发i/o场景下性能远超多线程;多线程仅适用于低并发、需共享状态的简单i/o任务,且绝不能用于cpu密集型任务。

asyncio 在高并发 I/O 场景下压倒性胜出;多线程仅在低并发、简单同步或需共享状态的 I/O 任务中仍有实用价值——但绝不能用于 CPU 密集型任务,也难以 scale 到数千并发。
asyncio.run() vs threading.Thread 启动开销差异
启动 1000 个 threading.Thread 实际会创建 1000 个 OS 线程,每个线程默认分配约 8MB 栈空间(Linux 下),内存瞬间飙升 8GB,且线程调度、上下文切换成本随数量指数增长;而 asyncio.run() 启动 1000 个协程,本质是往事件循环里塞 1000 个 Task 对象,总内存占用通常不到 10MB。
常见错误现象:OSError: can't start new thread 或进程被 OOM killer 杀死,基本就是误用 threading 处理高并发 HTTP 请求导致的。
- 实操建议:HTTP 爬虫、API 聚合类场景,直接用
aiohttp.ClientSession+asyncio.gather(),别碰requests+threading - 若必须用
threading,上限建议控制在 20–50 个线程,并配threading.Semaphore防爆 -
asyncio的协程调度完全在用户态,无系统调用开销;threading每次join()或wait()都涉及内核态切换
共享状态与锁的使用成本完全不同
多线程天然共享全局变量、模块级状态,看似方便,但一旦并发写入,就必须加 threading.Lock;而 asyncio 协程运行在单线程内,没有 GIL 争抢,但也没有“自动共享”——所有跨 Task 的变量都得手动管理,且 threading.Lock 在协程里会阻塞整个事件循环。
典型误用:async def fetch(): global counter; counter += 1 —— 这不是线程安全,也不是协程安全,纯属竞态灾难。
- 实操建议:需要计数/缓存等共享状态时,优先用
asyncio.Lock包裹临界区,而非threading.Lock - 避免在协程中调用任何阻塞函数(如
time.sleep()、json.loads()大字符串),改用await asyncio.sleep()或扔进loop.run_in_executor() - 若状态逻辑复杂,考虑用
asyncio.Queue做生产者-消费者解耦,比锁更清晰
CPU 密集操作会卡死 asyncio 事件循环
asyncio 是单线程模型,任何耗时的同步计算(比如 sum(range(10**7))、numpy.dot()、正则全文匹配)都会让整个事件循环停摆,所有其他协程冻结——这不是性能差,而是功能失效。
常见错误现象:接口响应延迟突增、超时堆积、监控显示 CPU 使用率低但请求积压严重——大概率是某个 await 后面藏着未识别的 CPU 密集代码。
- 实操建议:用
cProfile或py-spy record -p $(pidof python)抓热点,确认是否真在 CPU 上跑满 - 真正需要计算时,用
loop.run_in_executor(None, cpu_bound_func, *args)扔给默认线程池,或显式传concurrent.futures.ProcessPoolExecutor绕过 GIL - 注意:
run_in_executor有调度和序列化开销,别对毫秒级小计算滥用;大对象传参优先用mmap或multiprocessing.shared_memory
连接池与资源复用机制根本不同
aiohttp 的 ClientSession 内置连接池,自动复用 TCP 连接、支持 HTTP/2、能精细控制最大连接数与超时;而 threading + requests 默认每次新建连接,除非手动配置 urllib3.PoolManager,且线程间无法共享连接池实例。
容易踩的坑:用 threading 发 1000 个 requests.get(),结果触发目标服务的连接数限流,或本地端口耗尽(Address already in use)。
- 实操建议:始终把
ClientSession作为单例或 context manager 复用,别在每个协程里session = aiohttp.ClientSession() - 数据库场景同理:
asyncpg连接池远优于SQLAlchemy+threading的线程绑定连接 - 若必须混用,例如 FastAPI 接口里既有
await db.fetch()又有cpu_calc(),就明确分层:I/O 走await,CPU 走run_in_executor
真正难的不是选 asyncio 还是 threading,而是识别哪些代码表面是 I/O、实际是 CPU 密集(比如解析 10MB JSON、渲染模板、校验 JWT),这些地方不剥离,再好的并发模型也会被拖垮。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











