协程与 asyncio

5 min中级

协程与事件循环核心 API:asyncio.run、Task、TaskGroup、超时控制与异步上下文。

前置知识

学习目标

  • 掌握「事件循环」的核心机制、典型用法与常见陷阱
  • 掌握「什么是协程」的核心机制、典型用法与常见陷阱
  • 掌握「基础概念」的核心机制、典型用法与常见陷阱
  • 掌握「快速上手」的核心机制、典型用法与常见陷阱
  • 掌握「详细用法」的核心机制、典型用法与常见陷阱

事件循环

基本写法:运行协程 asyncio.run(<协程>)

# 运行顶层协程:内部创建、管理并最终关闭事件循环
async def main():
    print("hello")
asyncio.run(main())

基本写法:获取运行中的事件循环(推荐) asyncio.get_running_loop()

# 在协程内部获取当前正在运行的事件循环
# 没有运行中的循环时抛 RuntimeError——这正是想要的防护
loop = asyncio.get_running_loop()

基本写法:获取当前事件循环(旧接口,注意兼容) asyncio.get_event_loop()

# 3.12 起弃用"没有循环就隐式新建"的行为并发出告警;
# 3.14 起没有正在运行的事件循环时直接抛 RuntimeError。
# 新代码一律 asyncio.run() 启动,协程内部用 get_running_loop()
loop = asyncio.get_event_loop()  # 仅在协程内部才等价于 get_running_loop()

旧写法迁移:设置事件循环策略(3.14 已移除) asyncio.set_event_loop_policy(<策略>)

# 事件循环策略系统已在 3.14 移除,不要再写这类代码:
# if sys.platform == "win32":
#     asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# 现代做法:Windows 默认 Proactor 循环已覆盖绝大多数场景;
# 追求更高性能时在非 Windows 平台使用 uvloop:
# import uvloop
# uvloop.run(main())

异步上下文管理器

import asyncio

class AsyncDatabaseConnection:
    """异步数据库连接"""

    async def __aenter__(self):
        print("连接数据库")
        await asyncio.sleep(0.1)  # 模拟异步连接
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("关闭数据库连接")
        await asyncio.sleep(0.1)  # 模拟异步关闭

    async def query(self, sql):
        await asyncio.sleep(0.5)  # 模拟异步查询
        return f"查询结果: {sql}"

async def main():
    async with AsyncDatabaseConnection() as db:
        result = await db.query("SELECT * FROM users")
        print(result)

asyncio.run(main())

异步迭代器

import asyncio

class AsyncRange:
    """异步范围迭代器"""

    def __init__(self, count):
        self.count = count

    def __aiter__(self):
        self.current = 0
        return self

    async def __anext__(self):
        if self.current >= self.count:
            raise StopAsyncIteration
        await asyncio.sleep(0.1)  # 模拟异步获取
        value = self.current
        self.current += 1
        return value

async def main():
    # 使用 async for 遍历异步迭代器
    async for i in AsyncRange(5):
        print(f"获取到: {i}")

asyncio.run(main())

什么是协程

协程是一种比线程更轻量的并发方式。线程由操作系统调度,切换开销大;协程由程序自身调度,切换开销极小。你可以创建成千上万个协程而不会耗尽系统资源。

Python 的 asyncio 模块提供了编写协程的基础设施。在 asyncio 中,你用 async def 定义协程函数,用 await 等待异步操作完成。当一个协程在等待(比如等待网络响应)时,事件循环会自动切换到其他协程执行,从而实现并发。

基础概念

同步与异步

同步代码是按顺序执行的:一行代码执行完才执行下一行。如果某行代码需要等待(如网络请求),整个程序都会停在那里。

异步代码在等待时会自动切换到其他任务。比如你发起一个网络请求,在等待响应的同时可以做其他事情,等响应到了再回来处理。这样就不会因为一个慢操作阻塞整个程序。

async/await 语法

  • async def:定义一个协程函数。调用协程函数不会立即执行,而是返回一个协程对象
  • await:等待一个异步操作完成。await 只能在 async 函数中使用

协程与任务

  • 协程(Coroutine):async def 定义的函数的返回值,本身不会执行
  • 任务(Task):对协程的封装,由事件循环调度执行。使用 asyncio.create_task() 创建

快速上手

第一个协程

import asyncio

# 定义协程函数
async def say_hello(name, delay):
    """延迟后打印问候"""
    print(f"开始等待 {name}...")
    # 异步等待(不会阻塞事件循环)
    await asyncio.sleep(delay)
    print(f"你好, {name}!")

# 运行协程
asyncio.run(say_hello("世界", 2))

并发执行多个协程

import asyncio
import time

async def say_hello(name, delay):
    print(f"开始: {name}")
    await asyncio.sleep(delay)
    print(f"完成: {name}")
    return f"{name} 的结果"

async def main():
    start = time.time()

    # 并发执行三个协程
    results = await asyncio.gather(
        say_hello("任务A", 2),
        say_hello("任务B", 1),
        say_hello("任务C", 3),
    )

    duration = time.time() - start
    print(f"结果: {results}")
    print(f"总耗时: {duration:.1f} 秒")  # 约 3 秒,不是 6 秒

asyncio.run(main())

输出:

开始: 任务A
开始: 任务B
开始: 任务C
完成: 任务B
完成: 任务A
完成: 任务C
结果: ['任务A 的结果', '任务B 的结果', '任务C 的结果']
总耗时: 3.0 秒

三个任务并发执行,总耗时等于最慢的那个任务(3 秒),而不是所有任务时间之和(6 秒)。

详细用法

创建任务

import asyncio

async def process_data(data):
    """处理数据"""
    await asyncio.sleep(1)
    return f"处理完成: {data}"

async def main():
    # 方式一:create_task 创建任务并立即开始执行
    task = asyncio.create_task(process_data("测试数据"))

    # 在任务执行的同时可以做其他事情
    print("任务已创建,可以做其他事...")

    # 等待任务完成并获取结果
    result = await task
    print(result)

    # 方式二:gather 同时等待多个任务
    results = await asyncio.gather(
        process_data("数据1"),
        process_data("数据2"),
        process_data("数据3"),
    )
    print(results)

asyncio.run(main())

任务取消

import asyncio

async def long_task():
    """长时间运行的任务"""
    try:
        for i in range(10):
            print(f"进度: {i}/10")
            await asyncio.sleep(1)
        return "完成"
    except asyncio.CancelledError:
        print("任务被取消了")
        raise  # 重新抛出,让调用者知道任务被取消

async def main():
    task = asyncio.create_task(long_task())

    # 等 3 秒后取消任务
    await asyncio.sleep(3)
    task.cancel()

    try:
        result = await task
    except asyncio.CancelledError:
        print("主函数得知任务被取消")

asyncio.run(main())

超时控制

import asyncio

async def slow_operation():
    """模拟慢操作"""
    await asyncio.sleep(10)
    return "完成"

async def main():
    # 方式一:wait_for 设置超时
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=3)
        print(result)
    except asyncio.TimeoutError:
        print("操作超时")

    # 方式二:asyncio.timeout(Python 3.11+)
    async with asyncio.timeout(3):
        try:
            result = await slow_operation()
            print(result)
        except asyncio.TimeoutError:
            print("操作超时")

asyncio.run(main())

异步 HTTP 请求

import asyncio
import aiohttp

async def fetch_url(session, url):
    """异步获取 URL 内容"""
    async with session.get(url) as response:
        return await response.text()

async def main():
    # 使用 aiohttp 进行异步 HTTP 请求
    async with aiohttp.ClientSession() as session:
        # 并发请求多个 URL
        urls = [
            "https://httpbin.org/get",
            "https://httpbin.org/ip",
            "https://httpbin.org/headers",
        ]
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

        for url, result in zip(urls, results):
            print(f"{url}: {len(result)} 字符")

# 需要先安装 aiohttp: pip install aiohttp
asyncio.run(main())

异步文件操作

import asyncio
import aiofiles

async def read_file(path):
    """异步读取文件"""
    async with aiofiles.open(path, 'r', encoding='utf-8') as f:
        content = await f.read()
    return content

async def write_file(path, content):
    """异步写入文件"""
    async with aiofiles.open(path, 'w', encoding='utf-8') as f:
        await f.write(content)

# 需要先安装 aiofiles: pip install aiofiles

asyncio.Queue

asyncio.Queue 是协程间通信的安全方式:

import asyncio
import random

async def producer(queue, producer_id):
    """生产者:向队列中添加数据"""
    for i in range(5):
        item = f"产品-{producer_id}-{i}"
        await queue.put(item)
        print(f"生产者 {producer_id} 添加: {item}")
        await asyncio.sleep(random.uniform(0.1, 0.5))

async def consumer(queue, consumer_id):
    """消费者:从队列中取出数据"""
    while True:
        item = await queue.get()
        print(f"消费者 {consumer_id} 处理: {item}")
        await asyncio.sleep(random.uniform(0.2, 0.8))
        queue.task_done()

async def main():
    queue = asyncio.Queue(maxsize=10)

    # 启动生产者和消费者
    producers = [producer(queue, i) for i in range(2)]
    consumers = [asyncio.create_task(consumer(queue, i)) for i in range(3)]

    # 等待所有生产者完成
    await asyncio.gather(*producers)

    # 等待队列中所有项目被处理
    await queue.join()

    # 取消消费者(它们在无限循环中)
    for c in consumers:
        c.cancel()

asyncio.run(main())

常见场景

异步 Web 爬虫

import asyncio
import aiohttp
from time import time

async def fetch(session, url):
    """异步获取单个 URL"""
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
            return await resp.text()
    except Exception as e:
        return f"错误: {e}"

async def crawl(urls):
    """并发爬取多个 URL"""
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        return results

async def main():
    urls = [f"https://httpbin.org/get?id={i}" for i in range(10)]

    start = time()
    results = await crawl(urls)
    duration = time() - start

    print(f"爬取 {len(urls)} 个页面,耗时 {duration:.2f} 秒")

asyncio.run(main())

异步数据库操作

import asyncio

async def batch_insert(records):
    """批量异步插入数据"""
    tasks = [insert_record(record) for record in records]
    results = await asyncio.gather(*tasks)
    return results

async def insert_record(record):
    """异步插入单条记录"""
    # 模拟异步数据库操作
    await asyncio.sleep(0.01)
    return f"已插入: {record}"

async def main():
    records = [f"记录_{i}" for i in range(100)]
    results = await batch_insert(records)
    print(f"共插入 {len(results)} 条记录")

asyncio.run(main())

注意事项与常见错误

不要在协程中调用阻塞函数

在 async 函数中调用 time.sleep()、requests.get() 等同步阻塞函数会阻塞整个事件循环,其他协程都无法执行。应该使用对应的异步版本:

import asyncio
import time

# 错误:使用同步的 time.sleep
async def bad_example():
    time.sleep(5)  # 阻塞整个事件循环 5 秒

# 正确:使用异步的 asyncio.sleep
async def good_example():
    await asyncio.sleep(5)  # 不阻塞事件循环

如果必须调用阻塞函数,使用 asyncio.to_thread 在线程中执行:

import asyncio
import time

async def main():
    # 在线程中执行阻塞操作
    result = await asyncio.to_thread(time.sleep, 5)

忘记 await

调用协程函数时如果不加 await,协程不会执行:

import asyncio

async def my_coroutine():
    print("这行会执行吗?")

async def main():
    # 错误:没有 await,协程不会执行
    my_coroutine()

    # 正确:使用 await
    await my_coroutine()

asyncio.run(main())

asyncio.run 只能调用一次

asyncio.run() 会创建新的事件循环并运行。在已有事件循环运行时(如在 FastAPI 中),不能再调用 asyncio.run()。应该直接 await 协程或使用 asyncio.create_task()。

gather 的错误处理

asyncio.gather 中某个任务抛出异常时,默认会取消其他任务。如果需要获取所有结果(包括异常),使用 return_exceptions=True:

results = await asyncio.gather(
    task1(),
    task2(),
    task3(),
    return_exceptions=True  # 异常作为返回值,不会取消其他任务
)

进阶用法

使用 Semaphore 限制并发数

import asyncio
import aiohttp

async def fetch_with_limit(session, url, semaphore):
    """带并发限制的请求"""
    async with semaphore:  # 获取信号量,超过限制则等待
        async with session.get(url) as resp:
            return await resp.text()

async def main():
    # 最多同时 5 个请求
    semaphore = asyncio.Semaphore(5)

    async with aiohttp.ClientSession() as session:
        urls = [f"https://httpbin.org/get?id={i}" for i in range(50)]
        tasks = [
            fetch_with_limit(session, url, semaphore)
            for url in urls
        ]
        results = await asyncio.gather(*tasks)
        print(f"完成 {len(results)} 个请求")

asyncio.run(main())

使用 as_completed 按完成顺序获取结果

import asyncio

async def task(name, delay):
    await asyncio.sleep(delay)
    return f"{name} 完成(耗时 {delay} 秒)"

async def main():
    tasks = [
        asyncio.create_task(task("A", 3)),
        asyncio.create_task(task("B", 1)),
        asyncio.create_task(task("C", 2)),
    ]

    # 按完成顺序获取结果(而不是按创建顺序)
    for coro in asyncio.as_completed(tasks):
        result = await coro
        print(result)

# 输出顺序:B -> C -> A

asyncio.run(main())

子进程管理

import asyncio

async def run_command(cmd):
    """异步运行命令行命令"""
    process = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )

    stdout, stderr = await process.communicate()
    return {
        "returncode": process.returncode,
        "stdout": stdout.decode(),
        "stderr": stderr.decode(),
    }

async def main():
    result = await run_command(["python", "-c", "print('Hello')"])
    print(result["stdout"])  # Hello

asyncio.run(main())

在 FastAPI 中使用异步

FastAPI 原生支持 async/await,你可以直接在路由处理函数中使用:

from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.get("/slow")
async def slow_endpoint():
    """异步端点:等待时可以处理其他请求"""
    await asyncio.sleep(2)
    return {"message": "完成"}

@app.get("/fast")
async def fast_endpoint():
    return {"message": "立即返回"}

协程定义与调用

基本写法:定义协程函数 async def <函数名>(<参数>): <语句>

# 使用 async def 定义协程
async def fetch_data(url):
    await asyncio.sleep(1)
    return {"data": "result"}

基本写法:await 等待协程 await <协程>

# 在协程中等待另一个协程完成
async def main():
    result = await fetch_data("https://example.com")
    print(result)

基本写法:await 多个协程顺序执行 await <协程1>; await <协程2>

# 依次等待两个协程
async def main():
    r1 = await fetch("url1")
    r2 = await fetch("url2")

任务管理

基本写法:创建任务 asyncio.create_task(<协程>)

# 将协程封装为 Task 并调度执行
async def main():
    task = asyncio.create_task(fetch_data("url"))
    result = await task

基本写法:并发运行多个协程 asyncio.gather(<协程1>, <协程2>)

# 并发执行多个协程,按顺序返回结果
async def main():
    results = await asyncio.gather(
        fetch("url1"),
        fetch("url2"),
        fetch("url3"),
    )

基本写法:gather 异常处理 asyncio.gather(<协程>, return_exceptions=True)

# 异常作为返回值而非抛出
async def main():
    results = await asyncio.gather(
        fetch("url1"),
        fetch("bad_url"),
        return_exceptions=True,
    )
    for r in results:
        if isinstance(r, Exception):
            print("失败:", r)

基本写法:Go 1.0 风格任务组 async with asyncio.TaskGroup() as <组>:

# Python 3.11+ 结构化并发,任一任务异常则全部取消
async def main():
    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(fetch("url1"))
        t2 = tg.create_task(fetch("url2"))
    # 退出 with 块时所有任务已完成
    print(t1.result(), t2.result())

等待与超时

基本写法:等待第一个完成 asyncio.wait_for(<协程>, <超时>)

# 设置超时,超时抛出 TimeoutError
async def main():
    try:
        result = await asyncio.wait_for(fetch("url"), timeout=5.0)
    except asyncio.TimeoutError:
        print("超时")

基本写法:Python 3.11+ asyncio.timeout async with asyncio.timeout(<秒>):

# Python 3.11+ 超时上下文管理器
async def main():
    try:
        async with asyncio.timeout(5.0):
            result = await fetch("url")
    except TimeoutError:
        print("超时")

基本写法:等待首个完成 asyncio.as_completed(<协程列表>)

# 按完成顺序获取结果
async def main():
    tasks = [fetch(f"url{i}") for i in range(3)]
    for coro in asyncio.as_completed(tasks):
        result = await coro
        print("完成:", result)

基本写法:wait 返回两组任务 done, pending = await asyncio.wait(<任务集>)

# 返回已完成和未完成两组任务
async def main():
    tasks = [asyncio.create_task(fetch(f"url{i}")) for i in range(3)]
    done, pending = await asyncio.wait(
        tasks, return_when=asyncio.FIRST_COMPLETED
    )

休眠

基本写法:异步休眠 await asyncio.sleep(<秒>)

# 非阻塞休眠,让出控制权
async def main():
    await asyncio.sleep(1.0)
    print("1秒后")

队列

基本写法:异步队列 asyncio.Queue()

# 协程间安全传递数据
async def producer(q):
    await q.put("item")
async def consumer(q):
    item = await q.get()
async def main():
    q = asyncio.Queue(maxsize=10)
    await asyncio.gather(producer(q), consumer(q))

基本写法:带缓冲的队列 asyncio.Queue(maxsize=<大小>)

# 设置最大容量,满时 put 阻塞
q = asyncio.Queue(maxsize=5)
await q.put("data")
item = await q.get()
q.task_done()
await q.join()

锁与信号量

基本写法:异步锁 asyncio.Lock()

# 协程间互斥锁
lock = asyncio.Lock()
async def safe_update():
    async with lock:
        shared_resource += 1

基本写法:信号量 asyncio.Semaphore(<数量>)

# 限制并发数量
sem = asyncio.Semaphore(5)
async def limited_fetch(url):
    async with sem:
        return await fetch(url)

基本写法:事件 asyncio.Event()

# 协程间事件通知
event = asyncio.Event()
async def waiter():
    await event.wait()
    print("收到信号")
async def setter():
    await asyncio.sleep(1)
    event.set()

异步生成器

基本写法:定义异步生成器 async def <函数名>(): yield <值>

# 异步生成器函数
async def async_range(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

基本写法:遍历异步生成器 async for <变量> in <异步可迭代>:

# 异步迭代
async def main():
    async for num in async_range(5):
        print(num)

Python 3.13+ asyncio 增强

基本写法:Python 3.13+ asyncio.Runner asyncio.Runner()

# Python 3.13+ Runner 上下文管理复用事件循环
with asyncio.Runner() as runner:
    r1 = runner.run(fetch("url1"))
    r2 = runner.run(fetch("url2"))

基本写法:Python 3.13+ eager_task_factory loop.set_task_factory(asyncio.eager_task_factory)

# Python 3.13+ 协程立即开始执行而非延迟调度
async def main():
    loop = asyncio.get_running_loop()
    loop.set_task_factory(asyncio.eager_task_factory)