码农高天
asyncio并不提升运算速度,其单线程机制要求任务必须显式出让控制权,若直接顺序await协程将退化为串行阻塞,须借助create_task或gather填充I/O等待间隙。
AI-assisted summary of 【python】asyncio的理解与入门,搞不明白协程?看这个视频就够了。 — original source. Check the original for full context.
Source published:
Key ideas
- asyncio 运行在单进程单线程中,不具备多核并行计算能力,核心价值在于通过事件循环调度来利用 I/O 等待缝隙提升并发吞吐量。 (0:00)
- 调用 async def 协程函数仅生成协程对象而不会执行任何代码,必须转化为 task 注册进事件循环中才能被调度。 (0:00)
- 直接对协程对象进行 await 会产生隐式阻塞并串行化任务,利用 create_task 或 gather 提前注册才能实现真正的并发等待。 (8:30)
- asyncio 属于非抢占式调度,任务出让控制权只能依赖显式遇到 await 或函数执行完毕;若协程内存在死循环或无让步硬计算将直接卡死整个事件循环。 (5:00)
Detailed notes
## Background 该视频由技术博主“码农高天”于 2023 年 9 月 29 日发布在 YouTube 频道上。简介指出,随着 Python 协程环境日趋成熟稳定,许多开发者由于对其底层原理缺乏理解而陷入“编抄编试”的困境,本期视频旨在透彻拆解 asyncio 的底层运行机制。
## TL;DR 视频深入剖析了 Python asyncio 的本质,强调其依然运行在单进程单线程中,核心价值在于充分利用 I/O 等待时间而非提升计算速度。作者详细厘清了协程函数、协程对象与任务(Task)的严格区别,以及事件循环(Event Loop)非抢占式的显式调度逻辑。通过对比直接 await 协程导致的串行阻塞陷阱,视频演示了如何利用 asyncio.create_task 和 asyncio.gather 实现真正的并发等待与结果提取,帮助开发者建立清晰的底层心智模型。
## Atmosphere & style 整期视频呈现出极为扎实、冷静且直击痛点的技术布道风格,没有花哨的噱头,全靠清晰的逻辑推演与现场代码演示层层推进。高天的讲述平实亲切却切中要害,用生动的大脑调度比喻和时间线复盘,将抽象晦涩的异步概念拆解得透彻易懂。观看过程犹如坐在资深工程师身旁听其梳理技术全貌,能带给观众一种豁然开朗、彻底告别“盲目试错”的踏实感与掌控感。
## Bulletin - [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) asyncio 运行在单进程单线程中,不具备多核并行计算能力,核心价值在于通过事件循环调度来利用 I/O 等待缝隙提升并发吞吐量。 - [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) 调用 async def 协程函数仅生成协程对象而不会执行任何代码,必须转化为 task 注册进事件循环中才能被调度。 - [08:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=510s) 直接对协程对象进行 await 会产生隐式阻塞并串行化任务,利用 create_task 或 gather 提前注册才能实现真正的并发等待。 - [05:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=300s) asyncio 属于非抢占式调度,任务出让控制权只能依赖显式遇到 await 或函数执行完毕;若协程内存在死循环或无让步硬计算将直接卡死整个事件循环。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) asyncio 本质是单进程单线程模型,不存在多线程级的抢占式切换与竞争冒险,无法直接加速 CPU 密集型运算。 - [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) 调用 async def 函数只会返回一个协程对象(coroutine object),在顶层直接调用不会执行内部代码并会触发 RuntimeWarning。 - [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) asyncio.run 是进入异步模式的入口,负责创建初始化事件循环,并将传入的根协程包装为首个任务开始调度。 - [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) 协程(coroutine)与任务(task)在概念上截然不同,协程是未调度的代码逻辑,只有被包装成任务才能在事件循环中排队运行。 - [05:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=300s) 直接使用 await coroutine 会一次性完成打包任务、注册、交出控制权并等待四项工作,从而阻止后续任务被提前创建,导致任务退化为串行等待。 - [05:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=300s) 事件循环不具备抢占能力,CPU 控制权必须由任务通过遇到 await 或自身运行完毕显式让出;死循环会导致整个程序挂起卡死。 - [08:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=510s) asyncio.create_task 负责将协程包装为任务注册进事件循环,但不会立即交出控制权,允许主线程在当前执行流中快速注册多个并发任务。 - [08:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=510s) 当 await 后面作用于已经创建好的 task 时,它只声明依赖关系并让出控制权,使得事件循环可以在不同等待任务之间自由切换。 - [10:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=630s) 协程的返回值无法通过直接赋值 task 拿到,必须通过 await task 才能由事件循环解包并返回最终结果。 - [10:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=630s) asyncio.gather 提供了批量聚合调度的能力,可自动将传入的协程打包为 task 并在 await 时并发等待,返回按传参顺序严格保序的结果列表。 - [12:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=750s) 理解 asyncio 必须建立“事件循环充当大脑、非抢占任务排队协作”的心智模型,从而摆脱无序试错和拼凑异步代码的习惯。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) 认识 asyncio 的本质与 Coroutine 的定义
在很多开发者的印象中,asyncio 像是一个带有神奇魔法的多任务机制,但码农高天在开篇就明确指出了它的底层现实。在 Python 3.7 及以上的现代标准语法中,asyncio 本质上依然是一段非常正常的、运行在单进程单线程里的 Python 程序。“也就是说 asyncio 这件事情并不能提升你的运算速度”,码农高天直言,它最适合处理的是具有大量等待时间的 I/O bound 任务,网络通讯就是其中最典型的代表。它的核心是一个作为“大脑”的事件循环(event loop),面对着很多可以执行的任务(tasks)并决定到底去执行哪一个。与操作系统级的线程并发不同,asyncio 内部同一时间只能运行一个任务,不存在系统级的上下文抢占式切换,因此也不会出现竞争冒险(race conditions)。每个任务必须主动告知 event loop 自己暂停或完成了,其他任务才有机会运行。
要掌握 asyncio,首要任务就是彻底区分 **coroutine**(协程)与 **task**(任务)。在 Python 语境下,coroutine 通常包含两层含义: 1. **Coroutine function(协程函数)**:即所有由 `async def` 关键字开头的函数定义。 2. **Coroutine object(协程对象)**:当像调用普通函数一样写下 `main()` 时,它并不会像普通函数那样直接执行函数体并返回内部的返回值,而是返回一个协程对象。这种机制非常类似于生成器函数(generator function)。
码农高天现场演示了一段代码:直接在代码顶层执行 `main()`。结果终端中根本没有输出预期的 `hello world`,反而弹出了一个来自 Python 的 RuntimeWarning。“一个 coroutine function 当你调用它的时候返回的是一个 coroutine object,它不会运行任何这个 coroutine 里面的代码”,码农高天再三提醒观众牢记这个基础行为。要想让 coroutine 真正运行,必须满足两件事:一是进入 async 模式,让 event loop 接管程序;二是把 coroutine 变成 task。
从普通的同步代码(synchronized 模式)切换到异步事件循环模式,标准的入口函数是 `asyncio.run(coroutine)`。该函数主要完成两项工作: - 建立并初始化 event loop; - 将传入的根 coroutine 包装成该 event loop 中的第一个 task。
Event loop 启动后寻找可运行的任务,由于此时仅有一个 task,它便会调度并执行传入的 `main()`,终端这才依次打印出 `hello`、等待 1 秒并打印 `world`。
```mermaid flowchart TD accTitle: Coroutine 调用与进入 Event Loop 的过程 accDescr: 展示 async def 产生的 coroutine object 如何通过 asyncio.run 转化为 task 并交由 event loop 管理。 subgraph Definition["定义与直接调用阶段"] F["async def main()"] -->|"调用 main()"| O["Coroutine Object<br/>(不执行内部代码,报 Warning)"] end
subgraph Loop["启动 Event Loop 阶段"] O -->|"作为参数传入"| R["asyncio.run(...)"] R -->|"1. 创建"| EL["Event Loop (大脑)"] R -->|"2. 转换并注册"| T["Task 1 (main)"] EL -->|"调度执行"| T end ```
---
[05:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=300s) 单任务调度与直接 `await` 产生的串行等待瓶颈
当程序已经处于 async 模式时,单个 task 无法体现并发优势,必须学会向 event loop 中添加新 task。码农高天通过一个自定义的 `say_after(delay, what)` 函数展开讲解。该函数是一个由 `async def` 定义的协程函数,内部调用了同样返回协程对象的 `asyncio.sleep(delay)`。
将 coroutine 转变为 task 的第一种方法就是使用关键字 `await`。当代码执行 `await <coroutine>` 时,Python 在后台完成了四件事: 1. **包装与注册**:把该 coroutine 包装成一个 task,并向 event loop 注册通知存在一个新 task。 2. **声明依赖关系**:通知 event loop,当前的 task(比如 `main`)必须等待被 await 的新 task(`say_after`)执行完毕后才能继续往下走。 3. **出让控制权(yield)**:主动放弃 CPU,“告诉 event loop 我这个 task 现在干不了了,你先让别的 task 干活去吧”。 4. **获取返回值**:当 event loop 重新调度该父 task 时,提取该 coroutine 的返回值并赋给等待它的变量。
码农高天在代码中顺序写下了 `await say_after(1, 'hello')` 与 `await say_after(2, 'world')` 并运行。时钟记录显示:程序在 5 分 12 秒启动,约 1 秒后打印 `hello`,再过 2 秒(5 分 15 秒)打印 `world`,总耗时整整 3 秒。码农高天详细复原了整个运行轨迹:
- `asyncio.run(main())` 启动,创建 event loop 并将 `main` 包装为 task 放入其中; - Event loop 派发 `main`,执行输出 `start at`; - 执行到 `await say_after(1, 'hello')`,`say_after` 被包装成 task 注册进 event loop,`main` 声明需要等待它并交回控制权; - Event loop 转而执行 `say_after`,内部又遇到 `await asyncio.sleep(1)`,`sleep` 变成第三个 task,`say_after` 同样声明等待并交出控制权; - `sleep` 通知 event loop 1 秒后完成,1 秒后 `sleep` 结束;event loop 唤醒 `say_after` 打印出 `hello`,`say_after` 执行完毕出让控制权; - `main` 重新获取控制权,接着往下走到第二行,对 `say_after(2, 'world')` 重复了上述全套过程,再度交出控制权等待 2 秒。
码农高天强调,在整个运行过程中,event loop 无法强行夺回 CPU,“所有控制权的返回都是显式的”,必须由任务主动出让。任务交出控制权唯二的方式是:**遇到 `await`**,或者**函数自身运行完毕**。“所以说如果你有一个 task 里面有一个死循环,你的整个 event loop 就卡死了。”
这种直接 `await coroutine` 的写法暴露了一个严重问题:由于 `await` 承担了太多动作,它不仅包装任务,还立刻交出控制权并陷入等待,导致后续代码根本没机会执行,后面的任务压根还没被创建出来。这就导致本应并发等待的 1 秒和 2 秒变成了生硬的串行累计 3 秒。
---
[08:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=510s) 使用 `create_task` 实现真正的并发等待
“在你第一个 hello 等一秒的时候,为什么这个 event loop 不能直接让这个 world 运行,然后你俩一起等着,两秒之后不就结束了吗?”为了解决串行阻塞的问题,asyncio 提供了 `asyncio.create_task` 函数。
`create_task` 的参数同样是一个协程对象。它的关键作用在于分担了此前 `await` 的部分职责:它只负责将 coroutine 包装成一个 task 并立即注册进 event loop,通知其该任务处于可执行排队状态,但**它不会交出当前的控制权**。因此,正在执行的 `main` 趁着自己持有控制权,能够一气呵成地连续创建两个任务: - `task1 = asyncio.create_task(say_after(1, 'hello'))` - `task2 = asyncio.create_task(say_after(2, 'world'))`
紧接着,`main` 再去执行 `await task1` 和 `await task2`。当 `await` 后面接的是一个已经生成的 **task** 而非 coroutine 时,它就跳过了打包创建阶段,仅仅是告诉 event loop:“我需要等这个 task 完成,我现在把控制权交还给你;等它完成后,我再取回控制权并拿回返回值”。
再次运行测试,整个程序的耗时从 3 秒缩短到了 2 秒。其底层运行机制发生了决定性的改变: - 当 `main` 执行 `await task1` 挂起自身时,event loop 手中同时持有 `main`、`task1` 和 `task2` 三个任务; - `task1` 启动后遇到 `sleep(1)` 挂起,通知 event loop 自己需要等 1 秒; - 此时 event loop 并没有闲着,它检查任务队列,发现还有一个已注册就绪的 `task2` 可以调度,于是立刻执行 `task2`; - `task2` 内部的 `sleep(2)` 同样挂起出让控制权,两个计时器在后台同时倒计时; - 1 秒后 `task1` 率先就绪并打印 `hello`,再过 1 秒 `task2` 打印 `world`,总时间完全由耗时最长的任务决定。
这正是 asyncio 处理 I/O bound 场景的威力所在:在网络请求发送出去后的漫长等待时间里,CPU 可以立刻切去触发下一个网络请求,实现并发等待。
```mermaid flowchart TD accTitle: create_task 调度执行流 accDescr: 展示 main 连续创建 task1 和 task2 并发排队,并在 await 时交出控制权并发等待的生命周期。 subgraph MainTask["主任务运行 (持有控制权)"] M1["create_task(say_after 1) --> 注册 Task1"] M2["create_task(say_after 2) --> 注册 Task2"] M3["await Task1 (出让控制权并等待)"] M1 --> M2 --> M3 end
subgraph LoopSched["Event Loop 调度并发"] EL["Event Loop"] M3 -->|"交出控制权"| EL EL -->|"调度"| T1["执行 Task1 (Sleep 1s)"] T1 -->|"挂起出让"| EL EL -->|"调度"| T2["执行 Task2 (Sleep 2s)"] T2 -->|"挂起出让"| EL EL -->|"1秒到达"| T1Done["Task1 结束打印 hello"] EL -->|"2秒到达"| T2Done["Task2 结束打印 world"] end ```
---
[10:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=630s) 获取返回值与使用 `asyncio.gather` 优雅批量调度
在掌握了任务并发之后,码农高天进一步解答了两个高频实际开发问题:如何获取 coroutine 的返回值?以及如何避免机械式地写多行 `await task`?
获取返回值的第一原则是:必须通过 `await`。码农高天修改了代码,让 `say_after` 函数返回一个 f-string(例如 `f"{what} {delay}"`)。如果在代码中直接写 `ret = task1`,拿到的只是一个 Task 对象;必须显式写成 `ret = await task1`,Python 才会将底层 coroutine 真正的 return 提取出来。
但是,面对 10 个甚至上百个并发任务时,先写 10 行 `create_task` 再写 10 行 `await task` 显然非常冗长愚蠢。为此,asyncio 提供了专门用于并发聚合的工具函数——`asyncio.gather(*aws)`。码农高天对 `gather` 的运作机制作了全面剖析: - **参数兼容性强**:它可以接收若干个 coroutine、task,甚至是另一个 gather 返回的 future 对象。 - **自动包装**:传入的如果是 coroutine,`gather` 内部会自动先将它们统统包装为 task 并注册到 event loop 中,无需开发者手动调用 `create_task`。 - **返回 Future 对象**:`gather` 本身并不是协程函数,它返回一个由其管理的 `Future` 对象。 - **结果有序保序**:当使用 `await asyncio.gather(...)` 时,程序向 event loop 声明等待里面所有的 task 完成。最终返回一个包含所有结果的列表(list),该 list 中结果的排列顺序与传入参数的顺序严格一致,而与内部任务实际完成的先后时间无关。
在演示中,码农高天直接向 `gather` 传入了两个原生的协程对象:`await asyncio.gather(say_after(1, 'hello'), say_after(2, 'world'))`。执行结果显示,耗时同样保持在最优的 2 秒,并且直接以 `['hello 1', 'world 2']` 的 list 形式拿到了两个任务的返回值。它既省去了手动 `create_task` 的代码,又避开了早期直接 `await coroutine` 导致的串行耗时 3 秒的缺陷。
---
[12:30](https://www.youtube.com/watch?v=brYsDi-JajI&t=750s) 核心概念全景复盘与总结
在视频的尾声,码农高天抛开具体 API 细节,带领观众重新梳理并固化 asyncio 的核心系统认知,强调必须建立四个维度的底层心智模型:
1. **Event Loop 的大脑角色与非抢占调度**:脑海中必须树立“上层是一个作为大脑的 event loop,下层是若干可执行 task”的模型。Task 无法命令 event loop 执行某个特定任务,只能报告“我正在等什么”。由于不存在系统级抢占,任务一旦开始运行,只能由任务**显式出让控制权**(通过遇到 `await` 或函数执行完毕返回)。 2. **并发的真相是时间缝隙的利用**:协程虽然被称为并发,但“同时刻实际上只有一段代码在跑它只是想办法在利用这些代码中间的等待时间”。如果业务逻辑是纯 CPU 密集的硬运算,完全没有 I/O 阻塞或等待,协程对于性能提升毫无帮助。 3. **彻底分清 Coroutine 与 Task**:必须深刻认识到 coroutine 自身并不能直接运行,它只有被转化为 task 后,才能进入 event loop 排队调度。要清楚协程何时被隐式转换成了任务——直接 `await coroutine` 以及将协程传给 `asyncio.gather` 时,系统底层都在悄悄执行包装为 task 的操作。 4. **返回值的提取机制**:coroutine 的返回值隐藏在其执行流的末尾,必须通过类似 `res = await <task/future>` 的语法,才能从 event loop 手中将其解包赋值给变量。
码农高天总结道,这期视频讲得相对基础,但把每个内部推演环节剖析得很细,目的就是帮助大家建立底层的全局图景,理解程序到底为什么在跑、怎么在跑,从而彻底摆脱“去网上抄一点代码然后试试到底好不好使、不好使再换一段”的试错状态。最后,他向观众表达了感谢,并提醒大家关注与一键三连。
## Notable quotes > [05:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=300s) 如果你有一个 task 里面有一个死循环,你的整个 event loop 就卡死了。 — 码农高天
> [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) asyncio 这件事情并不能提升你的运算速度 — 码农高天
> [00:00](https://www.youtube.com/watch?v=brYsDi-JajI&t=0s) 一个 coroutine function 当你调用它的时候返回的是一个 coroutine object,它不会运行任何这个 coroutine 里面的代码 — 码农高天
## Mentioned Python, asyncio, Event Loop, Coroutine, Task, Future, 码农高天
## Video description 这期视频聊一下asyncio。python的协程环境已经逐渐稳定和成熟了,由于async的编程思路比较不同,很多人很难理解它背后的原理,只能照葫芦画瓢,编抄编试。看完这期视频,希望你就知道async到底是如何工作的了。
[Source](https://www.youtube.com/watch?v=brYsDi-JajI)