【学习笔记】Python异步编程:如何让程序“一心多用”还不乱套?
一、异步编程解决了什么问题?
同步编程中,遇到 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 的核心调度器,负责:
- 任务调度:决定哪个协程在何时执行
- I/O 监听:监听 socket、文件描述符等事件(基于 selector/epoll/kqueue/IOCP)
- 回调执行:在条件满足时执行注册的回调函数
- 定时器管理:处理延时任务

工作流程:
事件循环启动 → 从就绪队列取任务执行 → 遇到 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、超时等)。
对比总结
| 特性 |
|
|
| 返回值 | 结果列表 |
元组 |
| 异常处理 | 默认立即传播 | 异常不自动抛出,需手动处理 |
| 使用场景 | 需要所有结果 | 需要精细控制任务状态 |
串行 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 的子类 |
| 职责 | 只表示结果,不驱动操作 | 封装协程并驱动执行 |
| 创建 |
|
|
考点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 时主动让出控制权,事件循环可以调度其他协程执行。单线程内实现了并发(不是并行),没有线程切换开销。
六、面试回答思路
面试时被问“协程怎么工作的”,不要只背语法,按 “问题—机制—对比” 结构展开:
- 先说场景:“比如发10个HTTP请求,同步要串行等待约10秒;用协程,发起第一个请求后立刻
await,控制权交还事件循环,接着发第二个……10个请求几乎同时发出。” - 再拆关键动作:“
await不是‘等结果’,而是‘注册回调 + 暂停自己’;事件循环用 epoll 监听 socket 状态,一就绪就唤醒对应协程。” - 最后点明边界:“协程提升的是 I/O 并发效率,不是 CPU 计算速度;CPU 密集任务要用多进程或
run_in_executor。”
七、总结
async def 定义协程(调用不执行),await 挂起协程让出控制权,事件循环负责调度,Future 是结果占位符,Task 是 Future 的子类用于封装协程。整个体系在单线程内通过协作式调度实现 I/O 密集型任务的高并发。
八、学习链接
更多推荐


所有评论(0)