Python 中协程是什么?async 和 await 的作用是什么?
简化版
协程是可以在执行过程中主动暂停、稍后恢复的函数执行单元。async def 定义协程函数,调用后得到协程对象;await 用来等待另一个可等待对象完成,并把执行权交还给事件循环,让其他任务有机会运行。
详细版
基本写法:
import asyncio
async def fetch_data():
await asyncio.sleep(1)
return "done"
async def main():
result = await fetch_data()
print(result)
asyncio.run(main())
关键点:
async def定义的是协程函数;- 调用协程函数不会立刻执行完整逻辑,而是返回协程对象;
await只能出现在异步函数中;await等待 I/O、定时器、Task 等可等待对象;- 协程适合高并发 I/O,不适合直接加速 CPU 密集计算。
常见误区:
coro = fetch_data() # 这里只是创建协程对象
如果不 await coro,也不把它包装成 Task 调度执行,它不会按你预期完成,还可能出现“coroutine was never awaited”警告。
面试回答可以抓住一句:协程通过在等待点主动让出执行权,用一个线程管理大量 I/O 等待任务。
完整版教学
一、协程解决的核心问题是什么
很多服务端程序的瓶颈不是 CPU 不够,而是大量时间在等:
- 等数据库;
- 等 Redis;
- 等 HTTP 接口;
- 等磁盘或网络 I/O;
- 等定时器。
传统同步代码在等待期间会卡住当前执行流。多线程可以缓解,但线程多了会带来创建成本、上下文切换、锁竞争等问题。
协程的思路是:当一个任务等 I/O 时,主动暂停,把执行权让出去;事件循环继续调度其他可运行任务。这样一个线程也能管理大量并发连接。
二、async def 到底做了什么
普通函数:
def foo():
return 1
result = foo()
调用后会立即执行函数体并返回结果。
协程函数:
async def foo():
return 1
coro = foo()
调用后得到的是协程对象,不是最终结果。协程对象需要被事件循环驱动,通常通过:
await foo()
或者:
asyncio.create_task(foo())
才能真正参与调度。
三、await 的本质是等待并让出控制权
await 不是普通的阻塞等待。它等待一个可等待对象完成,同时把控制权交还给事件循环。
async def task():
await asyncio.sleep(1)
这里的 asyncio.sleep(1) 不会阻塞线程 1 秒,而是告诉事件循环:“这个协程 1 秒后再继续,你先运行别人。”
这也是异步代码能提升 I/O 并发的原因。
四、协程和线程的区别
协程通常由事件循环在单线程内调度,切换点主要出现在 await。线程由操作系统调度,可能在你不容易预期的位置切换。
对比:
| 维度 | 协程 | 线程 |
|---|---|---|
| 调度者 | 事件循环 | 操作系统 |
| 切换点 | 通常在 await | 抢占式,位置不完全可控 |
| 成本 | 更轻 | 更重 |
| 适合任务 | 高并发 I/O | 阻塞 I/O、兼容同步库 |
| CPU 密集 | 不适合直接加速 | Python 线程也受 GIL 影响 |
五、协程不是让代码自动变快
如果异步函数里写阻塞代码:
async def bad():
time.sleep(3)
事件循环会被卡住,其他协程也不能运行。异步编程要求整个调用链尽量使用非阻塞 API。
正确写法:
async def good():
await asyncio.sleep(3)
如果必须调用同步阻塞函数,可以考虑 asyncio.to_thread() 或线程池。
六、常见误区与追问
| 概念 | 含义 | 面试重点 |
|---|---|---|
| 协程函数 | async def 定义的函数 | 调用后返回协程对象 |
| 协程对象 | 可被等待的执行单元 | 不调度不会运行 |
await | 等待 awaitable 并让出控制权 | 只能在异步上下文中使用 |
| Task | 事件循环调度协程的包装 | 支持并发推进、取消、结果获取 |
- 误区:协程等于线程。 协程通常在事件循环线程内协作切换,线程由操作系统调度,两者成本和并发模型不同。
- 误区:async/await 能让 CPU 计算自动变快。 它主要提升 I/O 等待场景的并发效率,CPU 密集任务仍会占住执行线程。
- 误区:await 只是语法上的等待。
await的关键是等待期间把控制权交还事件循环,让其他就绪任务继续执行。 - 追问:为什么调用 async 函数不打印函数体里的日志? 因为只创建了协程对象,还没有被
await或创建成任务调度。 - 追问:什么对象能被 await? 协程对象、Task、Future,以及实现 awaitable 协议的对象。
- 追问:协程适合什么业务场景? 高并发网络请求、异步数据库访问、消息消费、爬虫等大量时间花在等待 I/O 的场景。
记忆钩子:
async def造协程,await让出控制权,事件循环负责在等待间隙推进其他任务。
七、加强记忆
协程的关键不是“函数前面加 async”,而是“在 await 点主动让出执行权”。async def 创建协程函数,调用得到协程对象;await 等待可等待对象并交还控制权。它适合高并发 I/O,不能自动加速 CPU 密集任务,也怕在协程里塞阻塞调用。