一、异步编程解决了什么问题?
同步编程中,遇到 I/O 等待(如网络请求、文件读写、数据库操作)时,CPU 会“傻等”着时间过去,这段时间本可以用来处理其他任务。
异步编程的核心价值就是:在等待 I/O 的时候去做别的事,用单线程实现高并发。通俗来说:异步编程就是让单线程也能干多件事,不等白等,高效非阻塞。

二、核心概念与用法
2.1 async def — 定义协程函数
使用 async def 定义的是异步函数(协程函数),调用后不会立即执行,而是返回一个协程对象。
async def my_coroutine():
await asyncio.sleep(1)
return "Done"
coro = my_coroutine()
print(type(coro)) # <class 'coroutine'>
协程对象和普通函数的区别在于:协程对象拥有 __await__ 方法,使其可被 await。
2.2 await — 挂起并让出控制权
await 关键字只能在异步函数内部使用,它的作用是:
- 挂起当前协程,保存执行状态(局部变量、指令指针)
- 让出控制权给事件循环
- 事件循环可以调度其他任务执行
- 当被等待的对象完成后,事件循环恢复该协程继续执行
⚠️ 关键理解:await 不是“等结果”,而是“注册回调 + 暂停自己”。
2.3 可等待对象(Awaitable)
await 后面跟的对象必须是 Awaitable(实现了 __await__ 方法),主要包括:
- 协程对象(async def 调用返回)
- Task(对协程的封装)
- Future(异步操作结果的占位符)
- 自定义实现了 __await__ 的类的实例
2.4 正确执行异步函数
必须通过事件循环来调度执行,最简单的方式是使用 asyncio.run():
import asyncio
async def async_task():
print("异步函数执行了")
asyncio.run(async_task()) # 正确方式
直接调用协程函数不会执行,还会触发 RuntimeWarning: coroutine was never awaited。
三、底层原理
3.1 协程的本质:从生成器到协程
Python 的协程建立在生成器(Generator) 基础之上:
- 协程对象和生成器对象一样,拥有 send()、throw()、close() 方法
- 内部维护着自己的栈帧和局部变量状态
- 每次遇到 await,协程把当前执行上下文保存进协程对象,然后返回控制权给事件循环
- 当被等待的对象就绪后,事件循环调用该协程的 send(None),从上次暂停处继续执行
协程经历了三个阶段演进:生成器 → @asyncio.coroutine 装饰器 → async/await 原生语法。
3.2 事件循环(Event Loop)— 异步编程的心脏
事件循环是 asyncio 的核心调度器,负责:

工作流程:
事件循环启动 → 从就绪队列取任务执行 → 遇到 await →
协程挂起,放入等待队列 → 继续处理就绪队列中的下一个任务 →
I/O 完成/定时器到期 → 事件循环唤醒对应协程 → 恢复执行
事件循环本质上是一个 while 循环,不断检查哪些协程已就绪,然后重新投入执行队列。
关键特性:
- 单线程:默认在主线程运行,无多线程开销
- 非抢占式:协程必须主动 await 才能让出执行权
- 如果协程中有长时间计算而不 await,会阻塞整个事件循环
3.3 Future — 异步结果的占位符
Future 代表一个异步操作的最终结果,是一个可等待对象。
import asyncio
async def main():
loop = asyncio.get_running_loop()
fut = loop.create_future() # 推荐用 loop.create_future() 创建
def set_result():
fut.set_result("异步操作完成!")
loop.call_later(2, set_result)
result = await fut # 等待 Future 完成
print(result)
核心要点:
- asyncio.Future 不应该被直接实例化,应使用 loop.create_future() 工厂函数
- Future 不是线程安全的
- 可以设置结果(set_result)或异常(set_exception)
- 可以绑定回调(add_done_callback)
3.4 Task — 协程的封装与调度
Task 是 Future 的子类,用于封装协程并将其提交给事件循环调度。
关键区别:
- Future:只是一个结果的占位符,本身不驱动任何操作
- Task:主动运行一个协程,并在协程完成时设置 Future 的结果
创建 Task:
python
task = asyncio.create_task(my_coroutine()) # 推荐方式
create_task 会立即将协程提交到事件循环,返回一个 Task 对象。
Task 状态转换:
- Pending:任务创建后尚未执行
- Running:事件循环正在运行该任务
- Done:协程完成,结果已设置
3.5 并发执行多个任务
asyncio.gather() — 并发执行并收集结果
async def main():
tasks = [
asyncio.create_task(task("任务1", 2)),
asyncio.create_task(task("任务2", 1)),
asyncio.create_task(task("任务3", 3))
]
results = await asyncio.gather(*tasks) # 等待全部完成,返回结果列表
gather 关注的是收集结果,按传入顺序返回结果列表。如果某个任务抛出异常,默认立即传播该异常。
asyncio.wait() — 更底层的并发控制
wait 更底层,返回 (done, pending) 两个集合。支持更细粒度的控制(如 FIRST_COMPLETED、超时等)。
对比总结
|
特性 |
gather |
wait |
|
返回值 |
结果列表 |
(done, pending) 元组 |
|
异常处理 |
默认立即传播 |
异常不自动抛出,需手动处理 |
|
使用场景 |
需要所有结果 |
需要精细控制任务状态 |
串行 vs 并发
⚠️ 常见误区:await coro1(); await coro2() 是串行执行,不是并发。要并发必须用 gather、create_task 或 TaskGroup。
四、实战案例
案例1:异步并发请求(最经典场景)
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = ["https://api1.com", "https://api2.com", "https://api3.com"]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for url, html in zip(urls, results):
print(f"{url}: {len(html)} 字符")
asyncio.run(main())
同步方式总耗时 ≈ 所有请求耗时之和;异步方式总耗时 ≈ 最慢那个请求的耗时。
案例2:Future 手动控制
import asyncio
async def main():
loop = asyncio.get_running_loop()
fut = loop.create_future()
# 模拟:2秒后设置结果
loop.call_later(2, lambda: fut.set_result("完成"))
# 同步任务转异步
def blocking():
import time
time.sleep(1)
return "同步任务结果"
fut2 = loop.run_in_executor(None, blocking) # 返回 Future
result = await fut2
print(result)
run_in_executor 可以将阻塞的同步函数放到线程池执行,返回 Future 对象。
案例3:超时控制
async def main():
try:
result = await asyncio.wait_for(my_coroutine(), timeout=5.0)
except asyncio.TimeoutError:
print("操作超时!")
五、高频面试考点
考点1:async def 和普通 def 的区别?
- async def 定义协程函数,调用返回协程对象,不会立即执行
- 普通 def 直接调用即执行
- 协程对象必须通过事件循环或 await 执行
考点2:await 到底做了什么?
- 挂起当前协程,保存执行上下文(局部变量、指令指针)
- 将控制权交还给事件循环
- 当被等待对象完成后,事件循环恢复协程执行
- 不是简单的“等待” ,而是“注册回调 + 暂停自己”
考点3:事件循环是什么?怎么工作的?
- 是 asyncio 的调度核心,本质是一个 while 循环
- 维护就绪队列和等待队列
- 从就绪队列取任务执行,遇到 await 就挂起并放入等待队列
- I/O 就绪/定时器触发后唤醒对应协程
- 基于操作系统的 I/O 多路复用机制(epoll/kqueue/IOCP)
考点4:Future 和 Task 的区别?
|
Future |
Task |
|
|
本质 |
异步操作结果的占位符 |
Future 的子类 |
|
职责 |
只表示结果,不驱动操作 |
封装协程并驱动执行 |
|
创建 |
loop.create_future() |
asyncio.create_task() |
考点5:asyncio.sleep(1) 返回的是什么?是协程还是 Future?
返回的是协程对象。当 await asyncio.sleep(1) 时,这个协程内部实际上创建了一个 Future,并把自身挂起在这个 Future 上。定时器到期后,回调触发 Future.set_result(None),进而恢复外层协程。
考点6:gather 和 wait 的区别?
- gather:返回结果列表,异常立即传播
- wait:返回 (done, pending),支持精细控制
考点7:为什么说协程能高效处理 I/O 密集型任务?
因为协程在等待 I/O 时主动让出控制权,事件循环可以调度其他协程执行。单线程内实现了并发(不是并行),没有线程切换开销。
六、面试回答思路
面试时被问“协程怎么工作的”,不要只背语法,按 “问题—机制—对比” 结构展开:
七、总结
async def 定义协程(调用不执行),await 挂起协程让出控制权,事件循环负责调度,Future 是结果占位符,Task 是 Future 的子类用于封装协程。整个体系在单线程内通过协作式调度实现 I/O 密集型任务的高并发。
八、学习链接
https://www.bilibili.com/video/BV1JsLDzAEGu?spm_id_from=333.788.player.switch&vd_source=565c1342b6f8b08590419749e9babc0a&p=6



