当前位置: 首页 > news >正文

别再误会 `async def`:一篇讲透 `asyncio` 事件循环、线程与并发并行的实战指南

别再误会async def:一篇讲透asyncio事件循环、线程与并发并行的实战指南

很多团队新人第一次接触 Python 异步编程时,都会有一个很自然的误解:既然函数前面写了async def,是不是代码就会自动变快?是不是就能自动利用多核 CPU 并行执行?

答案很明确:不会。

asyncio的核心不是“自动多核并行”,而是在一个线程里高效调度大量等待型任务。它擅长处理网络请求、数据库访问、消息队列、WebSocket、爬虫、实时数据流等 I/O 密集型场景;但如果你把 CPU 密集型计算直接丢进asyncio,事件循环反而会被堵住,整个程序看起来像“卡死”了一样。

这篇文章会围绕一个核心问题展开:

asyncio事件循环是什么?它与操作系统线程是什么关系?

并回答团队追问:

如何一句话讲清楚“并发”和“并行”的差异?

一句话版本先给出来:

并发是“同一时间段内处理多个任务”,并行是“同一时刻真正执行多个任务”。

接下来,我们把这句话拆开讲透。


一、从 Python 的“胶水语言”气质说起

Python 从诞生之初就强调简洁、可读和高生产力。它没有把开发者困在复杂的语法细节里,而是让人把精力放在业务逻辑、数据处理和系统设计上。

这也是为什么 Python 能成为很多领域的首选语言:

  • Web 开发:Django、Flask、FastAPI
  • 自动化脚本:文件处理、接口调用、运维任务
  • 数据科学:NumPy、Pandas、Matplotlib
  • 人工智能:PyTorch、TensorFlow、Scikit-learn
  • 爬虫与数据采集:requests、aiohttp、Scrapy
  • 工程工具链:CLI 工具、测试框架、CI 脚本

Python 常被称为“胶水语言”,因为它擅长把不同系统、服务、库、接口连接起来。而在现代后端开发中,一个程序往往不是一直在计算,而是在大量等待:等待网络、等待数据库、等待磁盘、等待第三方 API。

这正是asyncio的舞台。


二、先搞清楚:async def到底做了什么?

很多人以为:

asyncdeffetch_data():...

写了async def之后,函数就会自动异步执行。

实际上不是。

async def定义的是一个协程函数。调用它时,函数体并不会立刻运行,而是返回一个协程对象

asyncdefhello():print("hello")coro=hello()print(coro)

你会看到类似输出:

<coroutineobjecthello at 0x...>

真正让它运行起来,需要事件循环:

importasyncioasyncdefhello():print("hello asyncio")asyncio.run(hello())

这里的asyncio.run()会创建并启动一个事件循环,执行传入的协程,最后关闭事件循环。

所以第一条关键结论是:

async def只是声明“这个函数可以被异步调度”,不是自动开启线程,也不是自动并行。


三、事件循环是什么?

你可以把asyncio事件循环想象成一个非常忙碌但有条理的调度员。

它做的事情大概是:

  1. 维护一批待执行任务。
  2. 执行当前可以继续运行的协程。
  3. 当某个协程遇到await,并且需要等待 I/O 时,把它挂起。
  4. 去执行其他已经准备好的协程。
  5. 等操作系统通知某个 I/O 已经完成,再把对应协程恢复执行。

简化流程如下:

┌─────────────┐ │ 事件循环启动 │ └──────┬──────┘ ↓ ┌─────────────┐ │ 执行可运行任务 │ └──────┬──────┘ ↓ ┌─────────────┐ │ 遇到 await? │ └───┬─────┬───┘ │是 │否 ↓ ↓ 挂起等待 继续执行 ↓ 等待 I/O 完成 ↓ 重新加入可运行队列

换句话说,事件循环不是魔法,它本质上是一个任务调度器

它的强大之处在于:当某个任务在等待时,事件循环不会傻等,而是立刻切换去处理其他任务。


四、事件循环与操作系统线程是什么关系?

这是最容易误解的地方。

默认情况下,一个asyncio事件循环运行在一个操作系统线程中,通常就是主线程。

也就是说,下面这段代码虽然同时创建了多个任务,但它们默认并不是跑在多个 CPU 核心上:

importasyncioasyncdefworker(name):print(f"{name}start")awaitasyncio.sleep(1)print(f"{name}done")asyncdefmain():tasks=[asyncio.create_task(worker("task-1")),asyncio.create_task(worker("task-2")),asyncio.create_task(worker("task-3")),]awaitasyncio.gather(*tasks)asyncio.run(main())

输出大致是:

task-1 start task-2 start task-3 start task-1 done task-2 done task-3 done

这段程序总耗时大约 1 秒,而不是 3 秒。

为什么?

因为asyncio.sleep(1)模拟的是“等待”,不是“计算”。当task-1在等待时,事件循环切换去执行task-2task-2等待时,再切换去执行task-3

但注意,它们不是在三个线程里同时计算,而是在一个线程里快速切换。

所以第二条关键结论是:

asyncio的并发通常发生在单线程内,它依赖协程主动让出控制权,而不是操作系统强制抢占线程。


五、并发与并行:一句话讲清楚

团队新人问:“并发和并行到底差在哪?”

可以这样说:

并发是一个咖啡师同时照看多杯咖啡的制作流程;并行是多个咖啡师同时制作多杯咖啡。

再技术化一点:

  • 并发:多个任务在同一时间段内交替推进。
  • 并行:多个任务在同一时刻由多个执行单元真正同时运行。

对应到 Python:

asyncio 单线程事件循环:并发 多线程:可能并发,I/O 场景常用 多进程:可以真正并行,适合 CPU 密集型任务

六、为什么异步 I/O 会快?

假设我们要请求 5 个接口,每个接口耗时 1 秒。

同步写法:

importtimedefrequest_api(i):time.sleep(1)returnf"result-{i}"start=time.time()results=[]foriinrange(5):results.append(request_api(i))print(results)print("cost:",time.time()-start)

总耗时大约 5 秒。

异步写法:

importasyncioimporttimeasyncdefrequest_api(i):awaitasyncio.sleep(1)returnf"result-{i}"asyncdefmain():start=time.time()tasks=[asyncio.create_task(request_api(i))foriinrange(5)]results=awaitasyncio.gather(*tasks)print(results)print("cost:",time.time()-start)asyncio.run(main())

总耗时大约 1 秒。

这就是异步 I/O 的价值:不是让单个任务更快,而是让等待时间被充分利用起来。


七、危险误区:在协程里写阻塞代码

来看一个常见错误:

importasyncioimporttimeasyncdefbad_task():print("start bad task")time.sleep(3)print("end bad task")asyncdefticker():foriinrange(5):print("tick",i)awaitasyncio.sleep(1)asyncdefmain():awaitasyncio.gather(bad_task(),ticker())asyncio.run(main())

很多人以为bad_task()async def,所以不会阻塞。

但它里面用了time.sleep(3),这是阻塞调用,会直接卡住当前线程,也就是卡住整个事件循环。

正确写法是:

importasyncioasyncdefgood_task():print("start good task")awaitasyncio.sleep(3)print("end good task")

在异步代码里,要尽量避免这些阻塞操作:

time.sleep()requests.get()subprocess.run()大量 CPU 计算 普通同步数据库驱动 普通同步文件大读写

如果必须调用同步阻塞函数,可以用asyncio.to_thread()把它放到线程里执行。

importasyncioimporttimedefblocking_io():time.sleep(3)return"done"asyncdefmain():result=awaitasyncio.to_thread(blocking_io)print(result)asyncio.run(main())

八、CPU 密集型任务:别指望asyncio自动多核

假设我们写一个耗 CPU 的函数:

defcpu_heavy(n):total=0foriinrange(n):total+=i*ireturntotal

如果你这样写:

importasyncioasyncdeftask():returncpu_heavy(50_000_000)asyncdefmain():awaitasyncio.gather(task(),task(),task(),)asyncio.run(main())

这不会自动多核并行。

原因很简单:cpu_heavy()里面没有await,它会一直占着当前线程执行,事件循环没有机会切换任务。

对于 CPU 密集型任务,更合适的是使用进程池:

importasynciofromconcurrent.futuresimportProcessPoolExecutordefcpu_heavy(n):total=0foriinrange(n):total+=i*ireturntotalasyncdefmain():loop=asyncio.get_running_loop()withProcessPoolExecutor()aspool:tasks=[loop.run_in_executor(pool,cpu_heavy,50_000_000),loop.run_in_executor(pool,cpu_heavy,50_000_000),loop.run_in_executor(pool,cpu_heavy,50_000_000),]results=awaitasyncio.gather(*tasks)print(results)asyncio.run(main())

进程池可以绕开单个 Python 解释器进程内的执行限制,让多个 CPU 核心真正参与计算。

所以第三条关键结论是:

I/O 密集型任务优先考虑asyncio;CPU 密集型任务优先考虑多进程、向量化计算或底层扩展。


九、事件循环、线程、进程的关系图

可以用下面这个示意图帮助新人建立直觉:

操作系统 ├── 进程 A │ ├── 主线程 │ │ └── asyncio 事件循环 │ │ ├── Task 1 │ │ ├── Task 2 │ │ └── Task 3 │ └── 工作线程 │ └── 阻塞 I/O 函数 │ ├── 进程 B │ └── CPU 密集型计算 │ └── 进程 C └── CPU 密集型计算

在这个结构里:

  • 事件循环跑在线程里。
  • 协程任务跑在事件循环里。
  • 一个事件循环同一时刻只执行一个 Python 协程片段。
  • 遇到await时,协程把控制权交还给事件循环。
  • 线程可用于包装阻塞 I/O。
  • 进程更适合 CPU 密集型并行计算。

十、一个实战案例:异步批量请求接口

假设我们要批量请求用户资料接口。真实项目中可能是调用第三方服务、内部微服务或数据平台 API。

示例用asyncio.sleep()模拟网络延迟:

importasyncioimportrandomimporttimeasyncdeffetch_user(user_id):delay=random.uniform(0.5,1.5)awaitasyncio.sleep(delay)return{"user_id":user_id,"name":f"user-{user_id}","delay":round(delay,2),}asyncdefmain():start=time.time()tasks=[asyncio.create_task(fetch_user(i))foriinrange(1,11)]users=awaitasyncio.gather(*tasks)foruserinusers:print(user)print("total cost:",round(time.time()-start,2))asyncio.run(main())

这个程序不会按顺序一个个等待,而是把 10 个请求一起交给事件循环调度。实际耗时接近最慢的那个请求,而不是所有请求耗时之和。

但生产环境不能无限制创建任务,否则可能把对方服务打爆,也可能耗尽本机连接资源。我们需要加并发控制。

importasyncioimportrandom sem=asyncio.Semaphore(3)asyncdeffetch_user(user_id):asyncwithsem:delay=random.uniform(0.5,1.5)awaitasyncio.sleep(delay)returnf"user-{user_id}"asyncdefmain():tasks=[asyncio.create_task(fetch_user(i))foriinrange(10)]results=awaitasyncio.gather(*tasks)print(results)asyncio.run(main())

Semaphore(3)表示最多同时执行 3 个请求。这是异步项目中非常重要的工程习惯:并发不是越高越好,稳定才是王道。


十一、最佳实践:写好asyncio的 10 条建议

1. 不要把async def当成加速器

async def只是定义协程,不代表自动并行。

2. 异步代码里避免阻塞调用

错误:

asyncdefbad():time.sleep(1)

正确:

asyncdefgood():awaitasyncio.sleep(1)

3. 使用真正的异步库

比如:

requests->aiohttp/httpx.AsyncClient psycopg2->asyncpg redis-py sync->redis.asyncio

4. 用asyncio.gather()聚合任务

results=awaitasyncio.gather(task1,task2,task3)

5. 给外部请求加超时

asyncdefmain():try:result=awaitasyncio.wait_for(fetch_user(1),timeout=2)print(result)exceptasyncio.TimeoutError:print("request timeout")

6. 给并发加限制

sem=asyncio.Semaphore(10)

这在爬虫、批量接口调用、消息消费中尤其重要。

7. CPU 密集型任务不要直接放进事件循环

使用:

ProcessPoolExecutor multiprocessing NumPy/Pandas 向量化 C/Rust 扩展

8. 跨线程操作事件循环要小心

如果你在其他线程里想通知事件循环,应该使用线程安全方法:

loop.call_soon_threadsafe(callback)

或者:

asyncio.run_coroutine_threadsafe(coro,loop)

不要随意在多个线程里共享和操作同一个事件循环。

9. 学会取消任务

task=asyncio.create_task(fetch_user(1))task.cancel()try:awaittaskexceptasyncio.CancelledError:print("task cancelled")

取消任务在超时控制、服务关闭、资源回收中很常见。

10. 保持代码可读性

异步不是炫技。好的异步代码应该让人一眼看懂:

  • 哪些地方会等待
  • 哪些任务会并发执行
  • 超时如何处理
  • 异常如何传播
  • 资源如何释放

十二、常见面试级问题

问题 1:async def函数调用后会立刻执行吗?

不会。它返回一个协程对象。需要被awaitasyncio.create_task()或事件循环调度后才会执行。


问题 2:await的本质是什么?

await表示当前协程愿意暂停,把控制权交还给事件循环,等等待对象完成后再恢复执行。


问题 3:asyncio是多线程吗?

默认不是。asyncio的事件循环通常运行在单个线程中,通过协程切换实现并发。


问题 4:异步能提高 CPU 计算速度吗?

通常不能。异步主要提升 I/O 密集型程序的吞吐能力。CPU 密集型任务应该考虑多进程或底层优化。


问题 5:asyncio.create_task()和直接await有什么区别?

直接await会等待当前协程完成后再往下走:

result=awaitfetch_user(1)

create_task()会把协程包装成任务,交给事件循环调度,可以和其他任务并发推进:

task1=asyncio.create_task(fetch_user(1))task2=asyncio.create_task(fetch_user(2))result1=awaittask1 result2=awaittask2

更常见写法:

results=awaitasyncio.gather(task1,task2)

十三、给团队新人最实用的判断口诀

当你不知道该用同步、多线程、异步还是多进程时,可以先这样判断:

任务主要在等网络、数据库、磁盘? 优先考虑 asyncio 或线程池 任务主要在做大量计算? 优先考虑多进程、向量化、底层扩展 代码里用了同步阻塞库? 不要直接塞进 async def 任务数量很多但单个任务大多在等待? asyncio 很适合 任务数量少但每个都很吃 CPU? asyncio 不适合

再浓缩成一句工程建议:

asyncio解决的是“等待太多”的问题,不是“计算太慢”的问题。


十四、写在最后:理解事件循环,就是理解现代后端的一半

Python 的魅力不只是语法简单,而是它能让开发者用很低的心智成本连接复杂世界。asyncio正是这种能力在现代网络应用中的体现。

它不神秘,也不应该被神化。

它不是自动多核并行工具,不是性能银弹,也不是把所有函数前面加上async就能让系统起飞的魔法。

它真正擅长的是:

  • 让大量 I/O 等待任务有序推进
  • 提升服务吞吐能力
  • 减少线程切换和资源消耗
  • 让高并发网络程序更清晰、更可控

当你真正理解事件循环、协程、线程、进程之间的边界,你就不会再纠结“为什么我的 async 代码没变快”,而是能根据业务场景选择正确工具。

最后,把本文最重要的几句话送给正在学习 Python 异步编程的你:

async def不等于并行。
事件循环通常运行在一个线程里。
await是协程主动让出控制权。
并发是任务交替推进,并行是任务同时执行。
I/O 密集用异步,CPU 密集用多进程或计算优化。

如果你在团队里带新人,可以把这句话作为最终解释:

asyncio就像一个单线程调度员:谁在等 I/O,就先挂起谁;谁准备好了,就继续执行谁。它能让很多任务并发推进,但不会自动让代码在多个 CPU 核心上并行运行。

你在项目中遇到过哪些异步编程的坑?是事件循环被阻塞、任务取消失效,还是把 CPU 计算误塞进了协程?欢迎把你的经验写下来,因为每一个踩坑故事,都是后来者少走弯路的路标。

http://www.cnnetsun.cn/news/2080751.html

相关文章:

  • 不止于通信:用Android串口打造智能硬件控制中心(附完整项目源码)
  • 【AI面试临阵磨枪】为什么生产级 Agent 必须上 Harness,而不能只靠 Prompt?
  • 阿里开源模型Qwen3-4B快速体验:上传文档,让它帮你总结分析
  • 量子退火器热力学特性与Gibbs分布验证研究
  • BBDown终极指南:5步掌握B站视频下载神器
  • 一键部署多用户:Stable Diffusion v1.5 Archive共享服务搭建教程
  • 终极网页排版指南:如何通过Typography Handbook快速提升设计水平
  • 汽车HIL测试中RAG增强LLM的优化实践与应用
  • 智能体记忆系统构建指南:从向量检索到工程实践
  • Kaggle冠军的数据科学实战策略与竞赛方法论
  • spartan.ng测试策略:Jest单元测试与Cypress e2e测试最佳实践
  • vscode-settings快捷键与工作流:10个必备技巧大幅提升编码速度
  • hook-flow 高级应用:利用生命周期流程图调试复杂 React 组件
  • PHP Server Monitor配置完全手册:10个关键设置技巧
  • NCM音频解密技术深度解析:构建高效音乐格式转换的最佳实践
  • 终极文本摘要利器:Pointer-Generator Networks完全指南
  • 递归语言模型:突破大语言模型长文本处理瓶颈
  • ARM Compiler工具链优化与嵌入式开发实战
  • 基于DQN的超级马里奥AI训练:从环境搭建到奖励函数设计实战
  • 微信支付-支付宝支付回调通知处理
  • LeaderF与Git集成:如何在版本控制项目中高效搜索文件
  • Ouibounce终极指南:提升着陆页转化率的7-15%秘诀
  • 终极Vant Demo测试实践:3步打造高质量UI组件单元测试指南
  • LangGraph知识
  • JetBrains IDE评估期重置:架构优化与安全配置指南
  • Pixel Language Portal应用场景:开发者社区多语种技术问答智能路由系统
  • WeDLM-7B-Base算力适配:多GPU张量并行部署与通信开销实测分析
  • Phi-3.5-Mini-Instruct 工业视觉应用:与传统OpenCV算法结合实践
  • 3分钟搭建自己的电话号码定位系统:免费开源解决方案完全指南
  • 安卓APP Knots 3D 3D绳结软件 打结方法图解大全 高级免费版