60 lines
4.4 KiB
Markdown
60 lines
4.4 KiB
Markdown
---
|
||
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 等待。
|
||
|
||
### 【基础】asyncio 适合什么场景?不适合什么场景?
|
||
|
||
**参考答案**:适合 IO 密集、并发连接多的场景:调用大模型 API、HTTP 请求、数据库查询、WebSocket / SSE 长连接。不适合 CPU 密集计算(单线程,无法加速),这类任务用多进程或交给 C 扩展库。
|
||
|
||
### 【基础】调用 `async def` 函数但没有 `await` 会怎样?
|
||
|
||
**参考答案**:只会创建协程对象,函数体不会执行,Python 会给出 “coroutine was never awaited” 警告。这一点和 JS 不同——JS 的 async 函数调用后立即开始执行。
|
||
|
||
### 【基础】为什么在协程里不能使用 `time.sleep` 或 `requests`?
|
||
|
||
**参考答案**:事件循环是单线程的,阻塞调用会让整个线程停住,所有其他协程都无法运行,服务的并发能力退化为 1。应该用 `asyncio.sleep`、`httpx.AsyncClient` 等异步库;只能用同步库时用 `asyncio.to_thread` 放到线程池执行。
|
||
|
||
## 进阶
|
||
|
||
### 【进阶】`asyncio.gather` 和 `TaskGroup` 有什么区别?
|
||
|
||
**参考答案**:`gather` 在某个任务抛异常时立即把异常抛给调用方,但其余任务继续运行;加 `return_exceptions=True` 则把异常作为结果返回。`TaskGroup`(3.11+)是结构化并发:一个任务失败时取消组内其他任务,等它们全部结束后,以 `ExceptionGroup` 抛出所有异常,不会留下“孤儿任务”。
|
||
|
||
### 【进阶】怎么限制并发数?
|
||
|
||
**参考答案**:用 `asyncio.Semaphore(n)`,在每个任务的实际调用外层 `async with sem:`;或者用固定数量的 worker 从 `asyncio.Queue` 取任务。注意多个请求要共享同一个 Semaphore;多进程 / 多实例时需要 Redis 等全局限流。
|
||
|
||
### 【进阶】asyncio 的取消机制是怎样的?
|
||
|
||
**参考答案**:`task.cancel()` 请求取消,任务在下一个 `await` 点收到 `CancelledError`。任务可以捕获它做清理(关闭连接、记录状态),但应该重新抛出,否则取消失效。`asyncio.timeout` / `wait_for` 超时也是通过取消内部任务实现的。`CancelledError` 继承自 `BaseException`,所以 `except Exception` 不会误吞它。
|
||
|
||
## 场景题
|
||
|
||
### 【场景题】需要对 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 会把它放到线程池执行。然后重新压测确认。
|
||
|
||
### 【场景题】一个异步任务在服务关闭时总是卡住,可能是什么原因?
|
||
|
||
**参考答案**:任务里捕获了 `CancelledError` 却没有重新抛出(或者用了裸 `except:`),导致取消无效;或者任务在执行不响应取消的阻塞调用(同步 IO、`to_thread` 中的线程无法被强制停止);或者有后台任务没有被追踪和取消。修复:正确传播取消、给阻塞调用设超时、在应用的生命周期关闭阶段统一取消并等待后台任务。
|