Files
zhuangang/topics/python/asyncio/interview.md
T

80 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: python.asyncio.interview
title: asyncio 面试题
type: interview
domain: python
stack: [asyncio]
tags: [asyncio, 协程, 并发, 面试]
level: intermediate
related: [python.asyncio.coroutines-tasks, python.asyncio.concurrency-control]
week: [4, 16]
---
# asyncio 面试题
## 基础
### 【基础】协程、线程、进程有什么区别?
**参考答案**:进程有独立的内存空间,切换和通信开销大,能利用多核;线程共享进程内存,由操作系统抢占式调度,在 CPython 中受 GIL 限制不能并行执行 Python 代码;协程是用户态的、协作式调度的执行单元,在 `await` 处主动让出控制权,切换开销极小,单线程即可同时处理大量 IO 等待。
**追问**:Python 多线程适合什么场景?→ I/O 密集且必须使用同步库的场景;CPU 密集任务因为 GIL 用多进程。
### 【基础】asyncio 适合什么场景?不适合什么场景?
**参考答案**:适合 IO 密集、并发连接多的场景:调用大模型 API、HTTP 请求、数据库查询、WebSocket / SSE 长连接。不适合 CPU 密集计算(单线程,无法加速),这类任务用多进程或交给 C 扩展库。
**追问**:CPU 密集的任务混在异步服务里怎么办?→ 用 `loop.run_in_executor` 放到进程池,或者交给单独的任务队列 / 服务处理,避免阻塞事件循环。
### 【基础】调用 `async def` 函数但没有 `await` 会怎样?
**参考答案**:只会创建协程对象,函数体不会执行,Python 会给出 “coroutine was never awaited” 警告。这一点和 JS 不同——JS 的 async 函数调用后立即开始执行。
**追问**:怎么发现忘记 await 的问题?→ 运行时会出现 `coroutine was never awaited` 的警告;类型检查工具(mypy / pyright)和 linter 也能检查出来。
### 【基础】为什么在协程里不能使用 `time.sleep` 或 `requests`?
**参考答案**:事件循环是单线程的,阻塞调用会让整个线程停住,所有其他协程都无法运行,服务的并发能力退化为 1。应该用 `asyncio.sleep`、`httpx.AsyncClient` 等异步库;只能用同步库时用 `asyncio.to_thread` 放到线程池执行。
**追问**:必须使用同步库怎么办?→ 用 `asyncio.to_thread()` 放到线程池执行,或者换成异步库(`httpx.AsyncClient`、`asyncpg` 等)。
## 进阶
### 【进阶】`asyncio.gather` 和 `TaskGroup` 有什么区别?
**参考答案**:`gather` 在某个任务抛异常时立即把异常抛给调用方,但其余任务继续运行;加 `return_exceptions=True` 则把异常作为结果返回。`TaskGroup`(3.11+)是结构化并发:一个任务失败时取消组内其他任务,等它们全部结束后,以 `ExceptionGroup` 抛出所有异常,不会留下“孤儿任务”。
**追问**:`gather` 的 `return_exceptions=True` 有什么用?→ 一个任务异常时不会立即抛出,而是把异常作为结果返回,适合“部分失败可以接受”的批量调用。
### 【进阶】怎么限制并发数?
**参考答案**:用 `asyncio.Semaphore(n)`,在每个任务的实际调用外层 `async with sem:`;或者用固定数量的 worker 从 `asyncio.Queue` 取任务。注意多个请求要共享同一个 Semaphore;多进程 / 多实例时需要 Redis 等全局限流。
**追问**:信号量和限速有什么区别?→ 信号量限制同时进行的请求数;限速限制单位时间内的请求数(如每分钟 600 次),服务商通常两种都有限制,需要同时控制。
### 【进阶】asyncio 的取消机制是怎样的?
**参考答案**:`task.cancel()` 请求取消,任务在下一个 `await` 点收到 `CancelledError`。任务可以捕获它做清理(关闭连接、记录状态),但应该重新抛出,否则取消失效。`asyncio.timeout` / `wait_for` 超时也是通过取消内部任务实现的。`CancelledError` 继承自 `BaseException`,所以 `except Exception` 不会误吞它。
**追问**:捕获了 `CancelledError` 后应该怎么做?→ 做完清理后重新抛出;吞掉它会导致取消失效、`TaskGroup` / 超时无法正常工作。
## 场景题
### 【场景题】需要对 1 万条文本调用大模型生成摘要,服务商限制每分钟 600 次请求,怎么设计?
**参考答案**:用 Semaphore 或 worker 池限制并发,再加速率控制(令牌桶,每秒约 10 次);每次调用设超时;对 429、超时、5xx 做带抖动的指数退避重试,429 响应如果带 `Retry-After` 就按它等待;单条失败记录下来,最后汇总重跑;记录进度(已完成的 ID)以便中断后续跑;结果写入数据库而不是全放内存。规模更大时用消息队列分发给多个 worker。
**追问**:中途失败的任务怎么办?→ 每条记录保存处理状态,失败的记录按错误类型重试(限流错误退避重试,内容错误记录跳过),支持断点续跑。
### 【场景题】FastAPI 接口里调用了一个同步的 SDK,发现服务在压测时吞吐很低,为什么?怎么改?
**参考答案**:在 `async def` 路由里直接调用同步 SDK,阻塞了事件循环,所有请求排队。改法:换成 SDK 的异步版本;或者 `await asyncio.to_thread(sdk.call, ...)`;或者把路由定义为普通 `def`,FastAPI 会把它放到线程池执行。然后重新压测确认。
**追问**:`def` 路由和 `async def` 路由在这里有什么区别?→ `def` 路由会被 FastAPI 放到线程池执行,不会阻塞事件循环;`async def` 中直接调用同步 SDK 会阻塞整个事件循环。
### 【场景题】一个异步任务在服务关闭时总是卡住,可能是什么原因?
**参考答案**:任务里捕获了 `CancelledError` 却没有重新抛出(或者用了裸 `except:`),导致取消无效;或者任务在执行不响应取消的阻塞调用(同步 IO、`to_thread` 中的线程无法被强制停止);或者有后台任务没有被追踪和取消。修复:正确传播取消、给阻塞调用设超时、在应用的生命周期关闭阶段统一取消并等待后台任务。
**追问**:怎么给关闭过程设置上限?→ 用 `asyncio.timeout()` 或 `asyncio.wait_for()` 给关闭等待设置超时,超时后取消剩余任务并记录日志。