Python单线程协程依赖显式出让 控制权被夺即锁死循环

【python】asyncio的理解与入门,搞不明白协程?看这个视频就够了。 — 码农高天

码农高天

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)

中文简报

Python单线程协程依赖显式出让 控制权被夺即锁死循环 asyncio并不提升运算速度,其单线程机制要求任务必须显式出让控制权,若直接顺序await协程将退化为串行阻塞,须借助create_task或gather填充I/O等待间隙。 视频深入剖析了 Python asyncio 的本质,强调其依然运行在单进程单线程中,核心价值在于充分利用 I/O 等待时间而非提升计算速度。作者详细厘清了协程函数、协程对象与任务(Task)的严格区别,以及事件循环(Event Loop)非抢占式的显式调度逻辑。通过对比直接 await 协程导致的串行阻塞陷阱,视频演示了如何利用 asyncio.create_task 和 asyncio.gather 实现真正的并发等待与结果提取,帮助开发者建立清晰的底层心智模型。 asyncio 运行在单进程单线程中,不具备多核并行计算能力,核心价值在于通过事件循环调度来利用 I/O 等待缝隙提升并发吞吐量。 (0:00) 调用 async def 协程函数仅生成协程对象而不会执行任何代码,必须转化为 task 注册进事件循环中才能被调度。 (0:00) 直接对协程对象进行 await 会产生隐式阻塞并串行化任务,利用 create_task 或 gather 提前注册才能实现真正的并发等待。 (8:30) asyncio 属于非抢占式调度,任务出让控制权只能依赖显式遇到 await 或函数执行完毕;若协程内存在死循环或无让步硬计算将直接卡死整个事件循环。 (5:00)

Reading summary

码农高天指出,Python 的 asyncio 机制运行在单进程单线程中,本质上是通过事件循环调度任务来并发利用 I/O 等待时间,而非提升纯 CPU 密集运算的执行速度。 底层本质与协程对象定义 (0:00) 在 Python 3.7 及以上标准中,asyncio 依然是一段单线程程序,同一时刻只能运行一个任务。其核心是由事件循环(event loop)扮演大脑,负责挑选可执行的任务(tasks)。因为不存在操作系统级的抢占式上下文切换,程序内部不会发生竞争冒险(race conditions),任务必须主动出让控制权,其他任务才有机会获得执行。因此,asyncio 仅适用于包含大量等待时间的 I/O bound 场景,网络通讯是典型代表,对纯 CPU 计算任务没有加速效果。 理解该机制的前提是分清协程(coroutine)与任务(task)。由 async def 定义的是协程函数,直接像普通函数一样调用它只会返回一个协程对象(类似于生成器函数),不会执行内部代码,在顶层直接调用还会触发 RuntimeWarning。要使代码真正执行,必须调用 asyncio.run(coroutine),该入口函数会初始化事件循环,并将传入的根协程包装为第一个任务交由事件循环调度。 顺序 await 造成的串行阻塞瓶颈 (5:00) 当处于 async 模式后,直接在协程前使用 await 关键字虽然能将其转化为任务,但往往会造成串行执行。当代码执行 await <coroutine> 时,系统会完成四项操作:将协程包装成 task 并向事件循环注册、声明当前 task 依赖新 task、主动交出 CPU 控制权(yield),以及待其完成后提取返回值。 由于直接 await 协程会立即交出控制权并陷入等待,后续代码无法被推进,下一项任务甚至还未被创建。码农高天在示例中演示了依次 await say_after(1, 'hello') 与 await say_after(2, 'world'),结果总耗时为整整 3 秒。他强调,事件循环无法强制剥夺 CPU,控制权只能通过遇到 await 或函数执行结束显式出让;若任务内部存在死循环,整个事件循环都将被卡死。 利用 create_task 实现真正的并发等待 (8:30) 为了消除串行等待,asyncio 提供了 asyncio.create_task。该函数同样接收协程对象并将其注册到事件循环的就绪队列中,但其关键差异在于它不会主动出让当前的控制权。主任务可以一气呵成地连续创建多个后台任务(如 task1 与 task2),随后再去针对具体的 task 执行 await。 当 await 作用于已生成的 task 时,它仅声明依赖并交出控制权。此时事件循环能够交替调度已就绪的任务:当 task1 挂起进入 1 秒计时时,事件循环立即转去执行 task2 并开启 2 秒计时,两个计时器在后台同时倒计时。整个程序的执行时间从而缩短为 2 秒,完全由耗时最长的任务决定,实现了真正的时间缝隙复用。 返回值提取与 gather 批量聚合调度 (10:30) 获取协程的返回值必须通过 await 实现。若直接将 task 赋值给变量,拿到的只是 Task 对象本身;只有显式写成 ret = await task,系统才会将底层协程执行完毕后的 return 结果解包赋值。 面对多任务场景,写多行 create_task 与 await task 显得冗长,asyncio.gather(*aws) 提供了更为优雅的聚合方案。gather 可以接收协程、任务或 future 对象,若传入的是原生协程,它会自动在内部将其包装为 task 并注册进事件循环。gather 返回一个统一管理的 Future 对象,通过 await 挂起等待全部任务完成。最终返回一个包含全部结果的列表,列表中的数据顺序严格对应传入参数的顺序,不受各个任务实际完成先后的影响,兼顾了并发性能与代码简洁度。 掌握 asyncio 的核心在于建立正确的系统模型:事件循环是非抢占式调度的中枢,任务必须主动通过 await 或执行结束来让渡控制权;并发并非同时运行多段代码,而是利用 I/O 等待的缝隙调度已注册的任务。

Source quotes

[5:00] 码农高天: 如果你有一个 task 里面有一个死循环,你的整个 event loop 就卡死了。 [0:00] 码农高天: asyncio 这件事情并不能提升你的运算速度 [0:00] 码农高天: 一个 coroutine function 当你调用它的时候返回的是一个 coroutine object,它不会运行任何这个 coroutine 里面的代码

Original transcript

[0:00] 这期视频我们讲一下AsyncIO这也是之前有些观众想看的一个内容我们这一次主要是理解和入门asyncio所以我们讲的内容不会很难主要是帮大家建立一个概念AsyncIO在演变的过程中其实很多的用法在改变我这次讲的是最新的用法然后是python3.7以上才支持的也是推荐大家学新不学旧那async这个部分呢 是python引入的一个新的语法 但它并不是一个新的神奇的机制它本质上还是一段非常正常的python运行的单进程单线程的程序也就是说asyncIO这件事情并不能提升你的运算速度它比较适合处理那些需要等待的任务最典型的就是网络的通讯AsyncIO这个东西的运算核心其实就是一个event loop一个event loop它就像一个大脑它面对着很多可以执行的任务然后决定执行哪个任务就这么简单那在python的asyncIO里 同时执行的任务只能有一个它不存在系统级的上下的切换它跟现成不一样它需要每一个任务主动告诉event loop我这边结束了你可以让别的任务开始了因此它有一个好处就是不存在竞争冒险这样的问题你可以明确的知道我每一个任务什么时候停止运算想要使用asyncIO第一件事就是理解什么叫coroutine 什么叫task我们先说co routine在python的语境里 当我们说到coroutine的时候 我们一般会指两种东西 一个是coroutine function 一个是coroutine object我们看这边这行代码asyncDefmain这个asyncDefmain就是一个coroutine functionPython里面所有asyncdef开头的东西都叫coroutine function他定义了一个co routine的过程那python一般的函数当我们调用它的时候它返回的是这个函数的返回值对不对但是所有的coroutine function所有以async def这么定义的function当你调用的时候它返回的是一个coroutine object它的本质跟生成器函数有些像啊如果你知道那个是什么东西的话就是当你在做main括号的时候它并不会运行main里面的程序它只会返回一个coroutine object我们试一下大家看我们尝试了一下 我们运行了may 但是它并没有print hello world 当然python也给了我们一个warning这个是你要牢记的一个事情 一个coroutine function 当你调用它的时候返回的是一个coroutine object 它不会运行任何这个coroutine里面的代码好那怎么运行coroutine的代码呢我们需要两件事 第一是进入async的模式也就是进入event loop开始控制整个程序的状态第二就是把coroutine变成task我们先说如何进入async模式我们正常写的python代码运行的时候如果我们管它叫synchronized模式的话那么在synchronized模式下切换到asynchronized模式切换到让event loop开始控制一切基本上我们只用一个入口函数叫做 asinco.run那A型IO.run它的参数是一个co routine它会做两件事第一 他会建立起这个event loop 第二 他会把这个coroutine变成这个event loop里面的第一个task那event loop建立之后他就会去找哪个task可以执行那当然只有一个task所以他就会开始运行run给进来的co routine 我们试一下谢谢大家大家可以看到他先print了hello,等了一秒钟,然后print了world那这个asyncIO run呢 就是我们从synchronize模式变到asynchronize模式的入口 它的参数是一个coroutine 记住这些好 那只有一个task是没有什么意思的 对不对我们刚才也说过event loop的核心是它有很多很多个task然后它来决定哪个task要运行所以我们很重要的一件事情是当我们已经处于async模式下的时候我们如何增加task接下来我们就介绍几个把coroutine变成task让他可以排队执行的方法大家看我们左边有一个say after函数它显然是一个coroutine 对不对它前面是async def那实际上async io.sleep它返回的东西也是一个coroutine那这里呢就涉及到了第一个把coroutine变成task的方法就是用await先回忆一下我们刚才讲的这个say after函数调用之后返回是一个coroutine对不对它并不会被运行那么当你await一个coroutine的时候发生了如下几件事第一,这个coroutine被包装成一个task并且被告诉了event loop说这块有一个新的task第二,他会告诉 event loop我现在这个task需要等到这个say after task完成之后我才能 [5:00] 继续他建立了这么一个依赖关系第三 它会yill出去 也就是它会告诉event loop 我这个task现在干不了了 你先让别的task干活去吧最后当Event Loop再次安排它运行的时候它会把say after这个coroutine里面真正的返回值拿出来保存起来当然现在我们这里Away前面没有写一个什么等于那这个值就没了因为我们的say after是没有返回值的对不对好 那我们运行一下这段程序啊我们可以看到在5分12秒之后差不多一秒钟print了一个hello然后在两秒钟之后print了一个world最后5分15秒的时候结束了我们来还原一下整个过程发生了一个什么事情啊首先 async l run main 把这个 main 作为一个 task 给放到了 event loop 里Event loop寻找task,他发现只有一个taskmay然后他就开始让taskmay运行main在运行的时候首先print了一个start at然后他运行了say after这个coroutine function得到了一个coroutine objectAwait这个CoroutineObject把这个CoroutineObject变成了一个Task放回了EventLoop里同时告诉event loop我需要等待他然后把控制权交还给了event loop现在event loop里面有两个task一个是may 一个是say after 但是may运行不了may说我要等这个say after 那以文zoof就让这个say after先运行say after这里其实做了一个很相似的事情async iosleep也是一个coroutinesay after也是把它变成了一个task然后告诉event loop说我得等这个sleep完成了我才能运行然后await又把这个控制权转回给了event loop以文的佛陆皮看现在有三个task了对不对有一个main有一个say after还有一个sleep那这个sleep会告诉event loop说我一秒钟之后我就好了所以这个event loop就等了一秒钟一秒钟之后呢这个sleep就完成了然后event loop看现在有两个task一个main一个say after这个main要等say after 但是这个say after 整个东西已经完成了于是event loop就说say after 你来运行吧然后say after print了一个hello 然后say after也完成了于是又把控制权交还给event loopevent loop一看现在只有一个task了就成main了然后再把控制权给main这个时候main就把第一个say after完成了然后再做第二个say after做同样的事又等了两秒之后它完成所以整个过程用了3秒那在这个过程中我们注意一下所有控制权的返回都是显示的就是Event Loop并没有办法强行的从一个Task里边拿回控制权必须要这个Task主动把控制权交回去交回去的方式有两种第一个是await会交回第二个当这个函数运行完毕之后它会交回所以说如果你有一个task里面有一个死循环你的整个event loop就卡死了好 那刚才运行过程中你可能发现了一个问题对不对这个Hello要等一秒这个World要等两秒他俩为啥不能一起等这就应该是我们携成的意义 对不对在你第一个hello等一秒的时候为什么这个event loop不能直接让这个world运行然后你俩一起等着 两秒之后不就结束了吗那这个就是我们直接用await把一个coroutine变成task可能遇到的问题因为await需要做的事太多了它要变成task 变成task之后会主动把控制权交出去 还需要等这样后面的代码必须要等到它完成之后才能变成一个task 才能被征兆被翻牌为了解决这个问题 asyncIO给我们提供了createTask函数我们看这里用到的create task函数这个函数的参数呢也是一个coroutine依然记住啊这个say after调用之后返回的是一个coroutine它不会运行任何coroutine里面的代码那这个CreateTask函数会把这个Coroutine变成一个Task并且把这个task注册到event loop里面也就是说他分担了刚才await coroutine的一部分功能他把它包装成了一个task然后告诉event loop说ok这个task已经可以开始执行了但是现在event loop并没有办法执行这个task因为控制权还在main的手里对不对那这个时候main趁着自己有控制权他就做了第二个task task2也是告诉妹这还有一个新的task啊这叫say after 2也可以运行了那在这儿之后,他才开始Await Task 1, Await Task 2那我们之前说过Await后面是一个coroutine的时候它有那么多个功能对不对把它变成一个task然后把控制权交还对吧等它然后拿反馈值那当Await后面是一个task而不是一个coroutine的时候他就省略了把coroutine变成task这一步他就只是告诉event loop我需要这个task完成我把控制权交还给你并且在控制权回来的时候从这个task里面提取所需要的反馈值好我们跑一下这个程序试一下我们看这次这个程序就只用了两秒钟因为当它await task1的时候event loop里面实际上已经有了三个task是main task1 [10:00] 1和task2 当task1跟event loop说完说我要等一秒钟才能完成之后event loop闲来无事就能发现哎 这有一个task2还可以执行于是他又执行了Task2Task2跟他说我要两秒钟之后才能结束这样两个Task就可以同时进行等待了这也就是为什么async很适合解决一些网络通讯的问题因为网络通讯很多时间是在等待上的 也就是我们所谓的I/O bound task就是我真正的运算挺少的我就是等他的回复呢那等待的这个时间就可以其他的任务去干活这个函数没有涉及coroutine的return所以我们把这个函数稍微修改一下我们看我们把函数修改了一下主要就是告诉大家怎么拿到coroutine的return值我们在coroutine的function里面还是正常的就return了一个东西我们return了一个fStream对不对就把what跟你类拼到一起了那当你要拿这个返回值的时候一定是一个variable等于await taske而不是一个variable等于task1我们之前强调过Await有一个功能是把这个Task或者Coroutine的返回值拿出来如果你不用await是拿不到这个值的我们预行一下这个代码可以看到啊这个RED1跟RED2都被print出来了一个是hello1 一个是world2那有的人看到这可能说了那如果我有很多task呢我有比如说10个task我就写10行await task吗这是不是太蠢了确实是这样的正因如此呢Async L给我们提供了一个函数叫做Gather我们看Gather这个函数它不是一个coroutine啊 但是它会返回一个叫做future的东西这个future也是可以用await的那Gather这个函数能干嘛呢首先它的参数是若干个coroutine或者task两个都可以 甚至可以是Future 也就是Gather的Return值它还可以接着Gather 如果是Coroutine的话它首先会把它包装成Task 并且注册到EventLoop里然后它会返回一个future值当你await这个future的时候你就相当于告诉event loop我要等待这里面每一个task都完成我才可以继续同时会把这些Task的Return值放到一个List里然后返回回来我们看一下这些程序的结果可以看到这次的 return value 是一个 listlist的里面分别是 hello 1 跟 world 2那么这个list的顺序和里面task的顺序是一致的这样呢我们就不用一个一个Task awaitGather还有一个好处就是如果你给的是coroutine的话它会把每一个coroutine首先都变成task也就是说我们可以不用自己手动先去建立这些task了大家看这段代码我们现在给进去了两个coroutine而不是给两个task那在之前我们直接用Await版本里面这个要用三秒那我们希望他只用两秒 对不对 我们看一下结果可以看到这种方式在拿到了正确的返回值的同时他也只用了两秒因为Gather首先把这两个coroutine都变成了task然后在main做await的时候才把控制权交回给了event loop这样event loop在分配哪个task可以工作的时候它就有更多的task可以分配了尽管asyncIO里面还有不少的功能但是掌握了这几个基本上你就理解了asyncIO的核心理念了那我再强调一下前面提到过的几个重点首先你的脑海里面要建立一个event loop的概念上面有一个event loop作为大脑下面是若干可执行的tasktask是没有办法控制event loop去执行某一个task的他只能告诉event loop说我在等这个task最终是由event loop来决定下面要运行哪个task而event loop一旦开始运行task就必须要task显示的把控制权交还给event loop交还控制权的方式有Await和函数运行完毕所以尽管我们会说这种协程的方式是并发的但是同时刻实际上只有一段代码在跑它只是想办法在利用这些代码中间的等待时间所以如果你的代码里面并没有等待这件事的话协程对你的代码是没有帮助的然后务必分清什么是coroutine 什么是taskcoroutine只有变成了task才开始被执行要能知道什么时候coroutine被隐识的变成了task在你直接awaitcoroutine的时候跟你放到gather里的时候你放进去的都是coroutine但是他们会偷偷把它变成task之行coroutine不变成task是没办法执行的最后拿到一个coroutine的返回值是需要用await的一个变量等于Await什么东西好 这期内容讲的比较浅 但是比较细希望通过这种讲解能让大家真正的理解所谓的asyncIO背后到底是什么东西以及如何去做一些最基础的使用而不仅仅是去网上抄一点代码 然后试试到底好不好使 不好使再换另一段希望能帮助大家构建一个概念 [15:00] 到底什么东西在运行 为什么它会运行好 那这期视频就到这里 如果喜欢的话记得点一下关注 别忘了一键三联哦