异步编程基础:协程、语法、事件循环、Future 与 Task

说实话,刚开始接触异步编程的时候,我也挺懵的。一个函数前面加个 async,里面用个 await,怎么就变成非阻塞了?后来在 CI 测试脚本里被 I/O 等待坑了好几次,才彻底搞清楚这背后的机制。

这一节我们把核心概念拆开揉碎了讲。你只要理解一件事:异步的本质是把等待时间让给别人干活,而不是傻等。

协程:轻量级的 "可暂停" 任务

协程(Coroutine)说白了就是一个可以暂停执行、回头再继续的函数。它不像线程那样需要操作系统切换上下文,开销小得多。

我举个例子:你在煮咖啡的时候,水烧开要等 3 分钟。同步的做法是盯着水壶发呆 3 分钟;异步的做法是这 3 分钟去切水果、烤面包,水开了再回来处理。协程就是这个 "切水果" 的切换动作。

核心区别:

  • 线程:抢占式切换,由操作系统控制
  • 协程:协作式切换,由程序员控制(遇到 I/O 或等待时主动让出)
# 这是一个协程函数的定义
async def fetch_data(url):
    # 模拟网络请求,await 会暂停当前协程
    response = await http_get(url)
    return response.text

嗯,这里要注意:async def 定义出来的函数,调用后返回的是一个 协程对象,不是直接执行函数体。我刚开始老忘这一点,以为加个 async 就能飞起来,结果打印了个 <coroutine object> 给我看。

async / await 语法糖 —— 写异步就像写同步

async 和 await 是 Python 3.5 引入的关键字。它们的作用是让异步代码读起来像同步代码,避免回调地狱。

我个人习惯把这些关键字看作标记:

  • async 告诉解释器:"这个函数可能会暂停"
  • await 告诉解释器:"这里需要等一个异步操作完成,先把控制权交出去"

避坑指南:我曾经在 await 后面跟了一个普通函数,结果抛了 TypeError: object NoneType can't be used in 'await' expression。记住——await 后面只能跟可等待对象(awaitable),比如协程、Task、Future。

import asyncio

async def say_hello():
    print("Hello")
    await asyncio.sleep(1)   # 暂停 1 秒,不阻塞其他协程
    print("World")

# 运行协程
asyncio.run(say_hello())

事件循环 —— 异步的调度核心

事件循环(Event Loop)是整个异步编程的 "调度员"。它负责:

  1. 注册协程、Task、回调
  2. 轮询哪些任务已经准备好继续执行
  3. 把控制权交给可执行的任务

你想想看,如果没有事件循环,协程就像一堆停在跑道上的飞机,没人指挥起飞。在 Python 里,asyncio.run() 就是帮你自动创建并启动事件循环,跑完再关闭。

小提示:在 Jupyter Notebook 或交互式环境里,asyncio.run() 可能会报错(因为已经有事件循环在跑了)。这时候可以用 await 协程() 直接用,或者手动获取事件循环。

# 手动获取事件循环
loop = asyncio.get_event_loop()
loop.run_until_complete(say_hello())
loop.close()

Future 与 Task —— 执行与管理的利器

Future 是一个底层概念。你可以把它想象成一个 "将来会给我结果" 的占位符。它只有三个状态:

状态含义
Pending还没出结果,等待中
Finished / Resolved有结果了
Cancelled被取消了

Task 是 Future 的子类,也是我们平时最常用的。它直接把一个协程包装成可调度的任务,自动加入事件循环。

我记得有一次写 CI 测试脚本,需要并发发 10 个 HTTP 请求。如果用 await 一个接一个发,总耗时就是 10 倍延迟。换成 asyncio.create_task(),瞬间并发出去,总耗时 ≈ 最慢的那个请求。

async def main():
    # 创建两个并发任务
    task1 = asyncio.create_task(fetch_data("url1"))
    task2 = asyncio.create_task(fetch_data("url2"))

    # 等待所有任务完成
    results = await asyncio.gather(task1, task2)
    print(results)

asyncio.run(main())

注意:create_task 必须在已有事件循环的环境里调用。如果在函数外部直接调用,会报 RuntimeError: no running event loop。所以最好在 async def 内部或者事件循环已启动的情况下使用。

最后总结一张关系图,帮你理清这些概念怎么配合工作:

异步编程核心组件关系 事件循环 (Event Loop) 协程 (Coroutine) async / await 语法 Task Future 结果 / 回调 虚线表示间接/内部关系

看到这张图你就明白了:事件循环是心脏,协程是动作,Task 是把动作包装成可调度的任务,Future 是任务的 "承诺单",最终拿到结果。搞懂这层关系,写起异步 CI 脚本来就顺手了。

其实说白了,异步编程就是在 "等待" 这件事上做文章。我们不是为了快,而是为了 不阻塞。这个观念转过来,后面的内容你就很好接。