码农高天审查bilipod代码 指异步语法未建立多任务致程序实质退化为同步

【Code Review】把异步写成同步?async可未必就异步了! — 码农高天

码农高天

开发者盲目堆砌async与await却缺少并发任务与统一事件循环,所有并发收益实质仅由多线程提供,反让主程序架构徒增无意义的复杂度。

AI-assisted summary of 【Code Review】把异步写成同步?async可未必就异步了! — original source. Check the original for full context.

Source published:

Key ideas

  • 开源项目 bilipod 虽通篇使用 async/await 语法,但因在数据初始化与下载过程中从未并发创建第二个协程任务,其 I/O 等待时无其他任务可调度,架构本质上完全退化成了单线同步串行。 (5:00)
  • 项目的包结构与导入方式存在冲突,采用标准 src 目录却在内部全部使用绝对导入,导致其无法作为一个独立的 Python package 被外部引入,只能直接运行 main.py 入口文件。 (0:00)
  • 调度器线程在触发任务时会临时创建独立的 Event Loop,与主线程的主事件循环彻底割裂,导致协程级上下文切换机制完全失效,实质退化为线程调度与跨线程事件通知。 (10:00)
  • 盲目滥用 async/await 并未带来并发 I/O 性能收益,反而因混用线程与异步原语让整体工程架构变得极其晦涩且难以维护。 (10:00)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 11 月 23 日发布。视频内容是一期代码审查(Code Review),针对知识星球用户投稿的开源项目 bilipod 展开深入分析,旨在探讨项目中虽然使用了 async/await 关键字,但为何在实际运行中并没有真正实现异步并发的问题。

## TL;DR 高天在审查 bilipod(一个将 B站视频转为播客订阅源的工具)代码时,指出了其包结构引用混乱、交互与日志混淆以及数据库插入缺乏幂等性等设计缺陷。更核心的问题在于,项目在数据初始化和网络 I/O 下载过程中仅有单任务线性推进,看似写满协程语法实则完全同步串行。最后,作者在独立线程中使用 schedule 调度器并频繁创建独立的 Event Loop,导致主线程与后台任务之间的事件循环完全割裂,协程上下文切换彻底失效,使整个异步架构形同虚设。

## Atmosphere & style 视频风格专业、犀利且充满技术极客的幽默感。高天的讲解直击痛点,通过层层剖析代码逻辑,在看似合理的 async 语法中抽丝剥茧,精准挑出“假异步”与架构混乱的硬伤。整体观看体验既像是一场严肃严谨的工程把脉,又带着生动风趣的即兴吐槽,给盲目套用异步框架的开发者敲响了警钟。

## Bulletin - [05:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=300s) 开源项目 bilipod 虽通篇使用 async/await 语法,但因在数据初始化与下载过程中从未并发创建第二个协程任务,其 I/O 等待时无其他任务可调度,架构本质上完全退化成了单线同步串行。 - [00:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=0s) 项目的包结构与导入方式存在冲突,采用标准 src 目录却在内部全部使用绝对导入,导致其无法作为一个独立的 Python package 被外部引入,只能直接运行 main.py 入口文件。 - [10:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=600s) 调度器线程在触发任务时会临时创建独立的 Event Loop,与主线程的主事件循环彻底割裂,导致协程级上下文切换机制完全失效,实质退化为线程调度与跨线程事件通知。 - [10:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=600s) 盲目滥用 async/await 并未带来并发 I/O 性能收益,反而因混用线程与异步原语让整体工程架构变得极其晦涩且难以维护。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=0s) bilipod 是一个抓取 B站 视频或播放列表并转制为播客订阅源的工具,采用 Python 开发并依赖 TinyDB 与 HTTPX。 - [00:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=0s) 包内部模块依赖若采用绝对导入而非相对导入,将破坏包的独立分发与复用能力,限制其仅能从入口脚本启动。 - [00:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=0s) 用户交互展示与系统日志具有本质区别,Banner 等交互内容不应绑定在可随时被关闭或调整级别的 Logger 上输出。 - [00:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=0s) 文件与数据库备份逻辑应严格校验物理文件是否存在,仅判断父文件夹是否存在容易引发逻辑断层或潜在异常。 - [05:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=300s) 在循环体起始处盲目添加 sleep 无法在并发协程场景下实现有效的请求限流(Rate Limit),属于反直觉的错误模式。 - [05:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=300s) 数据转换逻辑应进行合理封装,避免将过多的临时对象与拼装步骤暴露在业务循环中,降低数据传递耦合。 - [05:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=300s) 持久化操作缺乏幂等性设计,对既有数据直接执行 insert 而非 upsert 会导致数据库脏数据与主键冲突风险。 - [05:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=300s) 协程并发的核心价值在于多任务在 I/O 等待时交替执行,没有多任务并发调度时,async/await 与纯同步阻塞无异。 - [05:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=300s) 视频抓取与下载过程中缺乏过滤机制,未跳过已入库内容,且线性逐个下载彻底放弃了异步网络 I/O 的性能优势。 - [10:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=600s) 协程上下文切换依赖所有任务共享同一个事件循环,跨线程临时创建独立的 Event Loop 会割裂协程协作链路。 - [10:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=600s) 第三方同步调度库(如 schedule)与 asyncio 混合使用时,若仅靠线程包裹加单发 Event Loop,会使主线程事件循环退化为空转等待。 - [10:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=600s) 如果最终的并发仅靠多线程实现,且主流程无并发协程,则主程序完全没有必要引入复杂的异步架构。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=0s) 项目背景、包结构与架构初始化分析

本期视频继续进行代码审查(Code Review),项目来自知识星球用户的投稿,名为 **bilipod**。该项目的核心功能是抓取哔哩哔哩(Bilibili)上的用户视频或播放列表(playlist),并将其转制成播客(podcast)订阅源。用户在配置文件 `config.yaml` 中定义各项参数,例如在 `feeds` 字段下指定每个播客的 ID、对应 B站的用户或播放列表来源以及相关页面设置。程序读取配置后,负责下载视频、转音频并生成播客内容。

```mermaid flowchart TD accTitle: Bilipod 启动与初始化流程 accDescr: 展示程序入口 main.py 到各组件启动与初始化的过程

subgraph Entry["程序入口与配置"] A["config.yaml 配置"] --> B["main.py (asyncio.run)"] B --> C["初始化数据库 TinyDB"] end

subgraph InitStage["初始化与任务分发"] C --> D["data_initialize (爬取与下载)"] D --> E["创建异步任务 update_episodes"] E --> F["配置 schedule 定时任务"] end

subgraph Background["后台服务"] B -.-> G["独立线程: Web Server"] F -.-> H["独立线程: run_scheduler"] end ```

在审阅项目的物理与逻辑组织方式时,高天首先指出了包结构(package structure)与导入模式之间的严重矛盾:

- **目录结构与导入方式的冲突**:项目采用了标准的 `src/` 目录组织形式,入口文件位于 `src/bilipod/main.py`,包根目录下也放置了 `__init__.py`。然而,代码内部在引用本地模块(local modules)时全部采用了绝对引用(如 `import bilipod.xxx`)。高天指出:“这种引用方法只有在你的入口程序刚好在这个 Source bilipod 里面的时候才能工作,而我们看他很多其他的库函数也是使用的这种绝对引用,也就是说他整个 package 没有办法作为一个真正的 package 被使用,他只能运行那个 main.py 文件才能跑得起来。”如果要将其作为标准包发布或导入,内部模块之间的依赖必须规范使用相对引用(relative import)。 - **交互输出(Interaction)与日志(Logging)的混淆**:在 `main.py` 中,程序在读取 `argparse` 参数后启动,并在开头打印了一个巨大的字符画 Banner,而这个 Banner 是通过 Logger 输出的。高天强调,记录日志与同用户交互在语义上存在本质差别:“显示给用户的东西应该有没有 log 都显示,他是跟用户互动的必需品,而 log 是可以开启、可以关闭、可以设置 level、可以写到另一个文件的。” - **数据库路径与备份逻辑断裂**:在通过 TinyDB 初始化数据库时,代码判断若父文件夹不存在则创建,若存在则使用 `shutil` 将数据库文件复制备份。然而如果父文件夹存在但数据库文件本身不存在,调用复制逻辑便存在隐患甚至抛出异常。高天认为此处存在阅读上的逻辑断链,即使作者有特殊意图,也至少应补充注释说明。 - **并发与后台任务分派**:程序使用 `asyncio.run(main())` 启动主协程,随后用一个独立线程启动 Web 服务对外提供文件访问;接着调用 `data_initialize` 解析抓取源并落库;紧接着用 `asyncio.create_task` 启动 `update_episodes` 任务;最后为每个播客通过 `schedule` 注册定时更新任务,并额外开启一个常驻后台线程以支撑调度器循环。

---

[05:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=300s) data_initialize 逻辑剖析与伪异步陷阱

深入到 `data_initialize` 函数内部,代码开始根据用户配置的 `feeds` 遍历处理播客数据,但这里的控制流暴露了许多代码设计与异步机制理解上的硬伤:

- **反直觉且无意义的 Sleep**:在提取每个 Feed 的配置时,循环体内第一步直接执行了 `await asyncio.sleep(0.5)`。高天吐槽道:“从理论上说你是不是应该先干活再 sleep,你不能先休息再干活对吧。”更关键的是,在标准协程并发架构中,单纯的 `sleep(0.5)` 无法起到防止爬虫被目标站点限流(Rate Limit)的作用,因为这半秒窗口期内其他协程任务依然会持续发起请求。 - **过多中间量导致接口抽象失效**:循环中先构造了一个空的 `Pod` 对象,通过其 UID 异步调用 `get_pod_info` 发起网络请求获取元数据,再拿返回的信息去更新该 `Pod`。高天认为这种写法将过多的中间量和中间步骤暴露在业务循环中,导致代码极度难读:“我个人的感觉是这个 for loop 里应该只有这么一行东西,就我的目的是从一个 feed 拿到一个 pod,就这么简单……这样你就能避免在几个数据结构之间来回来去的传数据。” - **数据库插入缺乏幂等保护**:获取到 `Pod` 数据后,程序直接无条件 `insert` 进 TinyDB 的数据表中。由于数据库文件可能由以往运行遗留,无脑插入必然产生脏数据与主键冲突。此处应该使用具有“存在即更新,不存在即插入”语义的 `upsert` 机制。此外,后续提取视频列表(`get_episode_list`)并调用 HTTPX 下载视频时,同样缺少过滤机制,导致重复抓取已有内容。 - **“伪异步”的串行执行真相**:整个 `data_initialize` 过程充斥着 `async` 与 `await`,但根本没有利用协程的真正优势。高天尖锐地指出了这一架构缺陷:

> “大家有没有意识到他刚才完全没有用上异步这件事?他写了很多 async await 对吧,但是我们说协程异步的核心是什么?是多任务并行对吧。当我们等待一个任务,当这个任务正在做 I/O 的时候,要有另外的任务可以做,这才叫异步,才叫协程对吧。而到目前为止,他并没有建立第二个任务……也就是说在他 initialize 的过程中,从头到尾都只有一个协程任务在运行。当他进行 I/O 操作的时候,并没有第二个任务可以补上,这一段看似异步的代码实际上是完全同步的。”

无论是查询播客详情还是使用 HTTPX 下载音视频流,代码全都是线性单线推进,前一个视频下载完成前绝不开始下一个,完全丧失了并发 I/O 的性能收益。

---

[10:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=600s) 调度机制缺陷与线程/协程事件循环割裂

随后审查转向整个工程中唯一调用了 `asyncio.create_task` 的组件——`update_episodes`。按常理,后台更新任务应当与定时调度配合实现高并发事件驱动,但其底层的设计再次陷入了逻辑混乱:

```mermaid flowchart TD accTitle: 线程与事件循环割裂模型 accDescr: 说明主线程与调度器线程运行在不同事件循环中,协程间无法实现控制权切换

subgraph MainThread["主线程 (Main Event Loop)"] direction TB MTask1["update_episodes (任务: while True 等待 Event)"] MTask2["阻塞等待任务 (仅维持事件循环不退出)"] end

subgraph SchedThread["调度器独立线程 (run_scheduler)"] direction TB S1["schedule 定时扫描轮询"] --> S2["run_async 回调触发"] S2 --> S3["临时新建并启动独立的 Event Loop"] S3 --> S4["运行 update_pod 协程"] S4 --> S5["执行 update_event.set()"] end

S5 -.->|"跨线程触发 Event (无协程切换)"| MTask1 ```

- **Update Episodes 的死锁等待**:`update_episodes` 的主体逻辑是一个 `while True` 死循环,其内部唯一的动作是 `await event.wait()`,等待某个全局事件被唤醒后去更新并下载单集信息。而触发该事件的 `set()` 方法则放在 `update_pod` 中。 - **调度器运行于新线程与新事件循环**:定时任务使用了第三方库 `schedule`,运行在独立的后台辅助线程(`run_scheduler`)中。调度器到点触发任务时,调用了自定义的 `run_async` 辅助函数。然而该函数每次执行,都会在调度器线程中**新建一个完全独立的 Event Loop** 来跑 `update_pod`。 - **协程上下文切换彻底失效**:高天解释道,协程级的上下文切换(Context Switch)或控制权让渡(Control Yielding),前提是所有协程运行在**同一个**事件循环中。由于 `update_pod` 是在新线程中的新事件循环里单发单关,而主线程的 `update_episodes` 则挂在主事件循环上,两者之间完全无法发生协程级切换,仅能依赖线程级调度与同步原语通知。此时主线程事件循环里唯一挂载的任务,就只剩下一个处于纯等待状态的挂起协程。 - **总结与反思**:高天对该架构作出了最终定性:“他写的这个协程程序是一个同步程序,他几乎没有任何协程级别的异步。他确实做到了一些异步,但是是通过线程来完成的。也就是说,他其实完全没有必要把主程序搞得这么复杂,他主程序可以搞成完全同步的,和现在效果是一样的。”如果开发者在不理解底层调度机制的情况下盲目滥用 `async`/`await`,非但无法压榨硬件与网络潜力,反而会让整个软件架构退化得晦涩难懂、难以维护。

## Notable quotes > [05:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=300s) 当我们等待一个任务,当这个任务正在做 I/O 的时候,要有另外的任务可以做,这才叫异步,才叫协程对吧。 — 高天

> [00:00](https://www.youtube.com/watch?v=makiSlq3MLs&t=0s) 显示给用户的东西应该有没有 log 都显示,他是跟用户互动的必需品,而 log 是可以开启、可以关闭、可以设置 level、可以写到另一个文件的。 — 高天

## Mentioned bilipod, 哔哩哔哩, 高天, TinyDB, HTTPX, asyncio, schedule, Python

## Video description Code Review是提升代码水平最快的方式之一,这期视频我们review一个比较复杂的项目的一部分。这个代码使用了异步的架构,然而出现了async就一定异步成功了么?

本次代码:https://github.com/sunrisewestern/bilipod

[Source](https://www.youtube.com/watch?v=makiSlq3MLs)

中文简报

码农高天审查bilipod代码 指异步语法未建立多任务致程序实质退化为同步 开发者盲目堆砌async与await却缺少并发任务与统一事件循环,所有并发收益实质仅由多线程提供,反让主程序架构徒增无意义的复杂度。 高天在审查 bilipod(一个将 B站视频转为播客订阅源的工具)代码时,指出了其包结构引用混乱、交互与日志混淆以及数据库插入缺乏幂等性等设计缺陷。更核心的问题在于,项目在数据初始化和网络 I/O 下载过程中仅有单任务线性推进,看似写满协程语法实则完全同步串行。最后,作者在独立线程中使用 schedule 调度器并频繁创建独立的 Event Loop,导致主线程与后台任务之间的事件循环完全割裂,协程上下文切换彻底失效,使整个异步架构形同虚设。 开源项目 bilipod 虽通篇使用 async/await 语法,但因在数据初始化与下载过程中从未并发创建第二个协程任务,其 I/O 等待时无其他任务可调度,架构本质上完全退化成了单线同步串行。 (5:00) 项目的包结构与导入方式存在冲突,采用标准 src 目录却在内部全部使用绝对导入,导致其无法作为一个独立的 Python package 被外部引入,只能直接运行 main.py 入口文件。 (0:00) 调度器线程在触发任务时会临时创建独立的 Event Loop,与主线程的主事件循环彻底割裂,导致协程级上下文切换机制完全失效,实质退化为线程调度与跨线程事件通知。 (10:00) 盲目滥用 async/await 并未带来并发 I/O 性能收益,反而因混用线程与异步原语让整体工程架构变得极其晦涩且难以维护。 (10:00)

Reading summary

码农高天在代码审查项目 bilipod 时指出,许多开发者在形式上堆砌 async 与 await,但由于缺乏并发任务调度与统一的事件循环,代码本质上仍是串行同步执行,甚至因线程割裂导致协程机制完全失效。 包结构规范与基础工程设计缺陷 (0:00) 在项目的物理架构与初始化阶段,bilipod 采用了标准的 src/ 目录,入口为 src/bilipod/main.py。然而内部模块互相引用时全部采用了绝对引用,导致该包无法作为独立的 Python package 被外部导入,用户只能在特定目录下直接运行 main.py。高天强调,面向发布的规范库必须使用相对引用组织内部依赖。 此外,程序在工程交互与状态管理上也存在边界模糊问题。启动时的大型字符画 Banner 被直接写入 Logger,而高天指出日志与用户交互具有不同语义,交互输出应脱离 Log 等级独立显示。在通过 TinyDB 初始化持久化文件时,代码仅判断父文件夹是否存在便尝试复制备份,在父目录存在但数据库文件缺失时容易抛出异常,暴露了逻辑上的断裂。 单任务串行执行导致的伪异步陷阱 (5:00) 深入数据初始化流程 data_initialize 后,高天指出了代码在协程理解上的典型误区。循环在抓取 Feed 前首先执行 await asyncio.sleep(0.5),这种反向休眠不仅违反先工作后休息的常理,也无法在并发场景下真正起到防爬虫限流的作用。同时,业务循环暴露了过多中间数据结构,在未做幂等检查的情况下直接 insert 数据库,缺乏 upsert 机制与已下载内容的过滤。 更核心的缺陷在于协程并发特性的完全丧失。尽管网络请求与音视频下载大量标注了 async 和 await,但整个初始化过程自始至终只有一个活动协程在推进,前一个任务完成前不会分派下一个任务。高天指出,异步的核心价值在于某个任务等待 I/O 时让渡控制权给其他任务执行,单协程串行等待使得整套代码在运行时退化为完全的同步执行。 多线程割裂事件循环使协程调度失效 (10:00) 在后台单集更新逻辑 update_episodes 中,程序设计引入了更加混乱的并发模型。主协程启动了一个在 while True 循环中挂起等待 Event 的任务,而真正触发该事件的 update_pod 却被委托给了 schedule 定时库。该定时调度器常驻在另一个独立的辅助线程 run_scheduler 中。 问题在于,调度器线程触发任务时调用 run_async,每次都会临时创建一个全新且独立的 Event Loop 来运行协程。高天解释称,协程让渡与上下文切换的前提是任务必须挂载在同一个事件循环内。主线程的事件循环与子线程的事件循环彻底割裂,两者只能退化依赖跨线程原语通知,主事件循环中唯一的任务长期处于等待挂起状态,协程级别的异步调度由此名存实亡。 高天总结认为,该项目虽然表面依赖协程框架,但实际并发效果完全依赖后台独立线程实现,主程序设计过度复杂且未产生任何异步收益;盲目套用 async/await 而忽视底层调度,只会让代码晦涩难懂且难以维护。

Source quotes

[5:00] 高天: 当我们等待一个任务,当这个任务正在做 I/O 的时候,要有另外的任务可以做,这才叫异步,才叫协程对吧。 [0:00] 高天: 显示给用户的东西应该有没有 log 都显示,他是跟用户互动的必需品,而 log 是可以开启、可以关闭、可以设置 level、可以写到另一个文件的。

Original transcript

[0:00] 好那这期视频依然是我们上单之后的code review让我们感谢阿里云的大力支持这次code review知识星球有人报名所以我们就选了知识星球一位用户代码我们来简单看一下 Readme他这个项目叫BillyPod它的核心功能是把 B站上用户的视频或者一个playlist变成一个podcast大概意思就是用户给这么一个configuration的YAMLYAML里能配置非常多东西对吧然后在这里fees这个地方你可以设置每个podcast的ID然后每一个podcast是从哪一个用户或者哪个playlist来的当然下面还有一些乱七八糟的page大概就是这么个意思先完成这个配置然后运行程序这个程序从B站上下载视频 转换成Podcast这个项比较大,我们只看中间的一部分特别细节的东西我们只简单点一下我们主要来聊一下架构和流程首先我们看一下它的结构啊它的项目结构是采取了这种sourced的方法然后入口文件是在source Billy Pot main.py那一般这种结构呢我们就会把整个这个source的部分打包成一个package大家可以看他实际上也是打包成了一个package他这里有一个 init.pi但是我们来看一下 import的方式前面这个部分是built-in没有问题接下来是第三方库下面这个部分是local的module当然大家观察一下他的local module使用的是绝对引用这就失去了把你整个项目做成一个package的意义那这种引用方法只有在你的入口程序刚好在这个SourceBillyPod里面的时候才能工作而我们看他很多其他的酷函数也是使用的这种绝对引用也就是说他整个package没有办法作为一个真正的package被使用他只能运行那个Main点派文件才能跑得起来那如果是这样的话,你就完全没有必要用source的这种结构你也没有必要把你这个项目做成一个大的package就这种结构和这种斜法基本上可以说是冲突的那这里如果你想把它做成一个大的package内部的引用就一定要使用相对引用好 我们接着往下看这个main.pi 前面有个大的banner这里是入口 然后ArcParse这些东西我们都不说读完了配置之后直接跑了这个may函数大家注意一下这里用的是asyncIO.run对吧也就是说它整个程序用的是协程在他的程序里也大量的用到这种await我们先来看这个logger啊给自己的程序打logging是一个非常好的习惯但是当你自己在写一个程序的时候,你要意识到login和正常的interact之间的差别就有些东西你是想记录到log上的但是有些东西你是真的想打印给用户看的有可能你的log本身默认是输入到Standard Alt的但是这两个东西与意识不一样的显示给用户的东西应该有没有log都显示他应该是跟用户互动的必需品而log是可以开启可以关闭可以设置level可以写到另一个文件的我可以想象你想把刚才那个banner展示给用户但是我觉得这个banner不应该属于login的一部分尽管默认情况下他们都会在你的standard out出出所以我觉得这里应该有一个区分这边处始化database稍微有点怪这个逻辑大家可以看这里的逻辑是,如果这个past parent,也就是这个文件夹不存在,就建立这个文件夹如果这个文件夹存在就把这个文件复制一份我不知道SHUT又是怎么处理这个地方的啊但是如果这个文件夹存在但是这个文件不存在会发生什么呢就是你这个db path 本身并不存在你把它复制一份会报错吗我不知道啊这块但是至少从阅读上来说的话,这块逻辑好像有一个断链如果你是特意这么写的我觉得我会在这块加一个注释啊说明一下这会怎么样下面TinyDB建立两个Table没问题啊这下面呢用一个县城单独跑了一个server来server文件啊这个没什么用不看下面首先根据用户给的configuration初始化了一下各种table在这里基本上就是写上我需要去爬哪些东西然后做哪些podcast下面新建了一个AsyncIO task,叫Update Episodes。这个update episodes等会我会看到它是更新我们要爬什么的最后,对于所有需要爬的podcast,用schedule job做一个定时也就是每隔一段时间就跑一下这个任务这个schedule照等会我们也会看到最后他又跑了一个县城啊来support他之前那个schedule照结构大致是这样啊我们看中间的几个函数从data initialize开始 [5:00] Server的部分我们不管啊,我们就看怎么从config变到了pod从我设置的这些费子里面拿到ID跟config拿了一个吸烟0.5秒Sleep的位置非常诡异就一般情况下我们Sleep都是为了防止爬虫过载对吧就是我们担心不断的给一个网站发请求然后这个网站会把我们block掉但是从理论上说你是不是应该先干活再sleep你不能先休息再干活对吧当然如果full loop长了这两个是差不多的第二,对于AsyncIO来说,如果你的程序写对了那你是没有办法通过简单的sleep来防止爬虫过载的因为sleep0.5不代表在这半秒之内不要给这个网站发hp request在这0.5秒之内还有其他的task可以干这件事当然他整个这个asyncL用的不对所以Sleep还真是有效的我们等会再说然后下面的代码呢我读起来就开始有点费劲了他首先在这里建立了一个Pod object然后又根据这个Pod的UID拿到了这个PodInfo这个get podinfo是有网络通讯的 所以这块有await紧接着他又用这个Pod info去更新了这个Pod大家有没有感觉到这块逻辑特别别扭就它展示了很多中间步骤和中间量导致这个for loop很难读我个人的感觉是这个full loop里应该只有这么一行东西就我的目的是从一个Feed拿到一个Pod 就这么简单所以我的loop里就是从一个Feed拿到一个Pod至于怎么从Feed拿到一个Pod 这是那个韩叔的事了这样你就能避免在几个数据结构之间来回来去的传数据这个部分是我读着比较费劲的然后他这块还有一个点好像有问题就是他拿到了pod之后他是insert回pod table的我们回过头看看建立database的时候他这个database并不一定是全新的但如果每次都是全新的这个database有没有意义了对吧也就是说这个database里面有可能保存着之前运行时候的数据或者说在之前运行的时候这个Pod table里面可能已经有这个Pod了所以尽管你是在data initialize这个看似在初始化的函数里你依然不应该把这个Pod无脑的给塞进这个table里这块呢可能用tinydb给一个upzert函数会更好就是如果这个Pub已经在这个table里了就update否则的话insert不然的话如果你有重复的pod那你是不是还有重复的episode就很麻烦了接下来根据刚才拿到的Pod也就是每个用户或者playlist下面所有的视频信息得到一个 Episode List我要下载哪些视频然后开始下载当然这款还是有一样的问题对吧既然之前可能已经有数据了那我有一些episode可能已经下载过了或者说有的pod我已经处理过了那这里是不是有可能造成重复的下载我还特意看了一下啊,他的这个getepisodelist并没有任何的filter就是这个Pod里面有多少episode就返回多少episode后面这个部分好像是跟web server有关的我们跳过这个就是Data Initialized的全部内容我不知道大家有没有发现一个问题啊这个问题是我觉得他整个项目最大的问题大家有没有意识到他刚才完全没有用上一步这件事他写了很多 async away 对吧但是我们说斜程异步的核心是什么是多任务并行对吧当我们等待一个任务当这个任务正在做IO的时候要有另外的任务可以做这才叫一步才叫协程对吧而到目前为止,他并没有建立第二个任务,我们看他刚才没有任何一个create task包括他的这个GapPodInfo里面,他的网络通讯也是只有await下面这里依然是线性的等待下载视频的时候用的是HTBX依然是线性的他没有建立一个新的任务他所有下载的代码都是这样的他直接Await Download URL也就是说在他initialize的过程中从头到尾都只有一个协程任务在运行当他进行IO操作的时候并没有第二个任务可以补上这一段看似异步的代码实际上是完全同步的就他没有利用到一部的任何优势它的整个代码库唯一一次使用到CreatTask就是这里他建立这个task是update episodes然而我们来看一下这个update episodes它的核心函数体是一个while true,而在每一个while true里面,它wait一个event,这个Event是每一次完成这个UpdatePod之后set的而update path跟我们刚才讲的initialization差不多,它基本上 [10:00] 清新一下episodes的信息更新一下需要下载哪些新的视频了而这个update path是什么时候运行的呢是我们刚才介绍的这个地方他有一个scheduled job对于每一个podcast他隔一段时间就跑一下这个update part我们来看一下这个schedule job的实现这个schedule job用了一个第三方库schedule这个schedule是怎么工作的呢还记不记得我们当时开了一个新县城跑run scheduler在这个run schedule里面就是这个schedule运行的地方我们有另外一个县城然后schedule这个酷不停的在检查有没有新的需要运行的工作如果到点了他就会跑一下这个 run ASIC大家注意一下这个 run async每一次run async,它新建了一个event loop。在这个event loop里面跑这个job而我们知道斜程的上下文交换或者说是控制权交换是只能在同一个event loop里进行的所以在这个RAM Async的过程中是没有携程级别的上海文交换的每一个run async跑一个update pod,update pod运行完,set一个update event。然后在主县城这里等待的update episodes他在里面去做更新做下载而所有的通过schedule job发起的任务实际上都是在另外一个现成的另外一个event loop里面进行的他们两件事之间没有任何协程级别的上海文切换只有现成级别的上海文切换而唯一一个和update episodes有携程级别上下文切换点在哪呢在这里这是主线程的那个event loop,唯一还在运行的task这个Task的任务就是等着而我们分析了这么多就是想告诉大家他写的这个斜程程序是一个同步程序他几乎没有任何邪程级别的一步他确实做到了一些异步但是是通过线程来完成的也就是说,它其实完全没有必要把主程序搞得这么复杂它主程序可以搞成完全同步的和现在效果是一样的所以斜程这个部分啊大家如果用的话还是要谨慎一点就你最好要知道你在干什么否则的话你把你程序代码弄得很难读还没有利用上这个斜程本身的优势那就亏了这个代码内容说实话比较多比较复杂啊我们一次code review肯定是没有办法把所有代码全都看完所以我就挑了几个我觉得比较适合讲的点来讲那我们还是希望在code review里给大家带来一些更深的思考就不仅仅是你这儿少个空格那块命名不对而是在代码的结构逻辑甚至设计上能给大家带了一些启发好 那这期视频就到这里吧 喜欢的话记得点关注 别忘了一件三连哦优优独播剧场——YoYo Television Series Exclusive