代表性面试主题

编程面试:Python asyncio 的 eager task factory 何时值得启用?

编程题困难
Offer.cc 编辑团队发布 更新

题干

一个 Python asyncio 服务大量调用带缓存的协程。请评估是否启用 asyncio.eager_task_factory,并说明任务执行顺序、异常、取消、TaskGroup、版本兼容和回滚验证。

题干与适用场景

一个异步服务在缓存命中时经常无需真正 I/O,但仍为每个协程创建任务并等待事件循环调度。团队想全局设置 asyncio.eager_task_factory 来减少调度开销。请判断收益是否足以承担语义变化,并给出安全的灰度方案。

Python 文档说明:启用后,协程在 Task 构造期间同步开始;只有遇到阻塞才调度到事件循环。该行为从 Python 3.12 起提供,且会改变任务执行顺序。

面试官考察点

重点观察候选人是否区分“同步完成”和“异步阻塞”,能否解释 eager 执行对公平性、异常传播、取消和 TaskGroup 的影响;是否只在测量证明收益的边界启用,保留版本检测、回滚和指标。

回答前需要澄清的问题

  • 服务运行的最低 Python 版本与所有部署环境是否一致?
  • 协程是读本地缓存,还是可能触发网络、数据库或锁等待?
  • 是否依赖任务创建顺序、事件循环公平性或 call_soon 回调顺序?
  • 失败、取消和超时是否由 TaskGroup 或请求作用域统一管理?
  • 优化目标是 CPU 调度开销、尾延迟,还是吞吐?基线数据是什么?

30 秒回答框架

“我不会直接全局开启。eager factory 让协程在构造 Task 时同步运行,缓存命中可省掉一次事件循环调度;遇到阻塞才排队。它改变执行顺序,可能让一个创建任务的循环长时间占用当前任务,也会改变异常何时暴露。先按 Python 版本和调用点灰度,测量命中率、事件循环延迟、尾延迟和错误率;不满足收益或破坏公平性就回退默认 factory。”

分步骤深入解答

第一步:建立默认与 eager 的执行模型

默认 create_task 会把协程安排到事件循环稍后运行;eager factory 在构造期间立即推进协程,直到返回、抛错或第一次阻塞。同步完成的任务可能根本不会进入调度队列。

python
loop.set_task_factory(asyncio.eager_task_factory)
task = asyncio.create_task(read_cached(key))

因此,不能把 eager 当作“更快的同一调度器”;它是可观察的执行语义变化。

第二步:定位适合的协程边界

适合候选通常是纯内存缓存、记忆化计算和高命中率的短协程。会访问网络、数据库、文件、锁或无界 CPU 的协程应保持短暂、可阻塞的路径,避免在任务构造阶段抢占当前执行流。

第三步:分析顺序与公平性

多个任务在循环中创建时,eager 协程可能按创建顺序立即完成,改变原本交错的结果顺序。大量同步命中还可能让当前任务连续执行很久,推迟定时器、I/O 回调和其他请求。应监控 event-loop lag,并限制单次批量创建量。

第四步:处理异常和取消

同步抛出的异常可能在 create_task 调用附近暴露,调用栈和捕获位置与默认调度不同。调用方仍须保存 Task 引用,并让 CancelledError 继续传播。不要用 eager factory 掩盖请求取消或把异常转成成功结果。

第五步:与 TaskGroup 和超时组合

TaskGroup 仍负责任务树、兄弟取消和 join,但组内任务可能在 create_task 时已完成或失败。用结构化的 try/except* 处理异常,并在外层设置共享超时;不要假设所有任务都先排队再同时开始。

python
async with asyncio.TaskGroup() as group:
    user = group.create_task(read_cached("user"))
    orders = group.create_task(read_remote("orders"))

第六步:版本与局部启用

该 API 从 Python 3.12 起提供,Python 3.14 的 create_task 还支持 eager_start 参数。多版本服务应在启动检查版本,并优先用局部任务或独立事件循环验证,而不是无条件修改全局 factory。

第七步:设计灰度、回滚与指标

先在缓存专用调用点启用,设置可关闭开关;比较默认与 eager 的 CPU、任务创建耗时、缓存命中尾延迟、event-loop lag、异常率、取消率和下游 QPS。发现公平性或错误回归时恢复默认 factory,不需要改写任务业务逻辑。

第八步:验证语义而非只测基准

测试同步返回、同步抛错、第一次 await、外部取消、TaskGroup 多错误、超时、递归创建任务、定时器公平性和混合缓存/远程批次。用事件记录断言顺序,避免只跑吞吐基准就宣布优化成功。

高质量示范回答

“eager factory 适合高命中率、短而确定的缓存协程,因为它省去一次事件循环调度;它不适合不可预测的 I/O 或长 CPU 路径。最大风险是同步执行改变顺序、公平性和异常暴露时机。我的方案是 Python 版本检查、局部灰度、保留默认回退,并同时观测 loop lag、尾延迟、取消和错误指标。TaskGroup 与共享超时仍保留,所有取消和资源清理照旧。”

常见错误

  • 把 eager 当作无语义变化的性能开关 → 顺序和异常时机改变 → 先写出两种执行模型并测量。
  • 对所有协程全局启用 → 慢 I/O 或 CPU 阻塞当前任务 → 只在短同步路径灰度。
  • 忽略 Python 版本 → 旧环境启动失败 → 启动时检查版本并保留默认 factory。
  • 只看平均吞吐 → 事件循环公平性退化 → 加入 loop lag 和尾延迟指标。
  • 认为 TaskGroup 行为完全不变 → 同步异常捕获位置错误 → 覆盖构造阶段失败和多异常测试。
  • 吞掉取消或清理异常 → 请求结束后仍有泄漏 → 保留取消传播并验证资源释放。

追问及应对

追问一:缓存协程同步返回时,Task 还会存在吗?

可能在创建阶段已经完成;不要依赖它一定经历事件循环调度。调用方仍应使用返回对象读取结果,并覆盖同步完成路径。

追问二:eager 是否保证任务按创建顺序完成?

不保证业务层的全局顺序。它只改变开始时机;遇到阻塞后仍受事件循环调度,多个任务的完成顺序必须通过显式协调或结果排序保证。

追问三:如何避免一批缓存命中饿死其他请求?

限制批量大小,必要时在批次间主动让出控制权,并监控 loop lag;也可仅对单个低成本调用点使用 eager。

追问四:发生回归如何回滚?

关闭开关并恢复默认任务工厂,确认指标回到基线,再保留带事件顺序和版本信息的样本用于定位。

追问五:eager_start 与 factory 有何关系?

eager_start 是创建单个 Task 时的显式选项;factory 是事件循环级默认策略。两者都必须按实际 Python 版本确认签名和优先级,不能混用旧版本假设。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具