Python 的异步编程并不是把所有函数前面加上 async 就结束了。理解生成器、async/await、阻塞与非阻塞操作,以及事件循环之间的关系,才能判断一段代码究竟能否并发执行。
这篇文章把这些概念串起来,并用一个可以直接运行的示例帮助你验证理解。
生成器为何是重要起点
普通函数通过 return 返回结果后就结束了,而生成器使用 yield 暂停执行,并在下一次迭代时从暂停位置继续。它不会一次性把所有数据加载到内存中,也体现了“暂停、恢复”的执行模型。
def numbers():
print("生成 1")
yield 1
print("生成 2")
yield 2
for number in numbers():
print("收到:", number)
运行时可以看到,函数会在每次 yield 处暂停。这种可暂停的控制流,为后来的协程和异步任务提供了重要思想基础。不过,生成器本身并不等于异步代码;它不会自动让阻塞的网络或文件操作变成非阻塞操作。
async 和 await 到底做什么
使用 async def 定义的函数称为协程函数。调用它时不会立即执行函数体,而是返回一个协程对象。只有把它交给事件循环,或者在另一个协程中使用 await,代码才会真正运行。
await 的含义不是“创建一个新线程”,而是告诉事件循环:当前协程暂时没有可继续执行的工作,可以把执行机会交给其他任务。
下面的例子模拟两个非阻塞等待任务:
import asyncio
async def fetch(name: str, delay: float) -> str:
print(f"{name} 开始")
await asyncio.sleep(delay)
print(f"{name} 完成")
return f"{name} 的结果"
async def main() -> None:
results = await asyncio.gather(
fetch("任务 A", 1.0),
fetch("任务 B", 0.5),
)
print(results)
if __name__ == "__main__":
asyncio.run(main())
将代码保存为 async_demo.py,然后运行:
python async_demo.py
任务 B 会先完成,但整个程序大约只需要等待 1 秒,而不是把两个等待时间简单相加。这是因为 asyncio.sleep() 会暂停当前协程,同时让事件循环运行其他就绪任务。
阻塞代码与非阻塞代码
异步程序的并发能力取决于协程是否愿意把控制权交还给事件循环。await asyncio.sleep() 是非阻塞等待;而在协程中直接调用 time.sleep(),会阻塞整个事件循环。
import asyncio
import time
async def blocking_task() -> None:
print("阻塞任务开始")
time.sleep(2)
print("阻塞任务完成")
async def non_blocking_task() -> None:
print("非阻塞任务开始")
await asyncio.sleep(0.5)
print("非阻塞任务完成")
async def main() -> None:
await asyncio.gather(blocking_task(), non_blocking_task())
asyncio.run(main())
虽然这里使用了 gather(),但 time.sleep(2) 运行期间事件循环无法调度其他协程,非阻塞任务也会被拖住。实际项目中,常见的阻塞来源包括同步 HTTP 客户端、同步数据库驱动、文件 I/O,以及复杂的 CPU 密集型计算。
可以这样改造阻塞调用:
import asyncio
import time
def blocking_work() -> str:
time.sleep(2)
return "完成"
async def main() -> None:
result = await asyncio.to_thread(blocking_work)
print(result)
asyncio.run(main())
asyncio.to_thread() 会把同步函数放到线程中执行,避免它直接卡住事件循环。它更适合 I/O 型阻塞操作;对于真正的 CPU 密集型任务,进程池或专门的任务队列通常更合适。
事件循环与常见判断题
事件循环负责管理协程和任务:当一个任务遇到可等待的异步操作时,它会暂停当前任务,并尝试运行其他已经准备好的任务。
理解下面几条规则,可以解决大多数异步编程测验题:
async def定义的是协程函数,调用它只会得到协程对象。- 协程对象需要通过
await、asyncio.run()或任务调度 API 执行。 asyncio.create_task()会把协程安排到当前运行中的事件循环中。asyncio.gather()可以并发等待多个可等待对象,并按传入顺序返回结果。await不等于开启线程,也不等于并行执行 CPU 计算。- 单线程事件循环可以并发处理 I/O 等待,但不能消除同步阻塞代码的影响。
- 一个事件循环中不能随意嵌套调用
asyncio.run();在已经运行的协程里通常应直接使用await。
使用前的检查清单
把同步代码改成异步代码时,可以逐项确认:
- 外层调用链是否已经进入异步上下文。
- 网络、数据库等依赖是否提供真正的异步 API。
- 是否误用了
time.sleep()或同步客户端。 - 多个独立 I/O 操作是否适合使用
gather()。 - 任务是否需要超时、取消和异常处理。
- CPU 密集型工作是否应该交给进程池或后台任务系统。
异步编程的关键不是语法数量,而是控制权如何在事件循环中的任务之间流动。掌握“协程何时暂停、谁负责恢复、哪些调用会阻塞”这三件事,才能从生成器自然过渡到 asyncio,并准确判断一段 Python 异步代码的实际行为。