asyncio 异步编程完全指南

asyncio 适合 I/O 密集型 任务(网络请求、文件读写、数据库查询)。CPU 密集型任务用 multiprocessing 或 concurrent.futures.ProcessPoolExecutor。 Python 3.7+ 推荐的入口,创建事件循环、运行协程、关闭循环: gather 参数说明: return_when 可选值: 异步代码中调用同步阻塞函数(如读文件、调用同步库)会阻塞整个事件循环。应用 run_in_executor 放到线程池/进程池执行: TaskGroup 在任意子任务失败时会自动取消其余任务,并将所有异常汇总为

分享

官方文档:https://docs.python.org/zh-cn/3/library/asyncio.html
最后更新:2026-03-29


1. 核心概念

事件循环、协程、任务

概念 说明
事件循环(Event Loop) asyncio 的调度核心,负责运行协程、处理 I/O 事件
协程(Coroutine) async def 定义的函数,调用后返回协程对象,不会立即执行
任务(Task) 对协程的封装,提交给事件循环并发运行
Future 低层原语,表示一个异步操作的最终结果
await 挂起当前协程,将控制权交还事件循环,等待目标完成后继续

同步 vs 异步 I/O 模型

同步 I/O:
  请求 A → 等待 → 结果 A → 请求 B → 等待 → 结果 B   (串行,等待期间 CPU 空闲)

异步 I/O:
  请求 A → 挂起 → 请求 B → 挂起 → 结果 A 到达 → 继续 A → 结果 B 到达 → 继续 B
  (并发,等待期间处理其他任务)

asyncio 适合 I/O 密集型 任务(网络请求、文件读写、数据库查询)。CPU 密集型任务用 multiprocessingconcurrent.futures.ProcessPoolExecutor


2. 基础语法

async def 和 await

import asyncio

async def fetch_data(name: str, delay: float) -> str:
    print(f"{name} 开始")
    await asyncio.sleep(delay)  # 模拟 I/O 等待,不阻塞事件循环
    print(f"{name} 完成")
    return f"{name} 的结果"

# 运行协程
asyncio.run(fetch_data("任务A", 1.0))

asyncio.run()

Python 3.7+ 推荐的入口,创建事件循环、运行协程、关闭循环:

async def main():
    result = await fetch_data("任务A", 1.0)
    print(result)

asyncio.run(main())  # 程序入口,只调用一次

3. 并发运行多个协程

asyncio.gather() — 并发等待所有结果

import asyncio

async def task(name: str, delay: float) -> str:
    await asyncio.sleep(delay)
    return f"{name} done"

async def main():
    # 并发运行,总耗时约等于最长的那个(2s),而非串行的 1+2+3=6s
    results = await asyncio.gather(
        task("A", 1),
        task("B", 2),
        task("C", 3),
    )
    print(results)  # ['A done', 'B done', 'C done'](按提交顺序返回)

asyncio.run(main())

gather 参数说明:

参数 类型 默认值 说明
*coros_or_futures coroutine / Future 必填 要并发运行的协程或 Future
return_exceptions bool False True 时异常作为结果返回而非向上抛出
# return_exceptions=True:某个任务失败不影响其他任务
results = await asyncio.gather(
    task("A", 1),
    broken_task(),    # 会抛出异常
    task("C", 1),
    return_exceptions=True,
)
# results = ['A done', SomeException(...), 'C done']
for r in results:
    if isinstance(r, Exception):
        print(f"任务失败:{r}")

asyncio.create_task() — 立即启动任务

async def main():
    # create_task 立即将协程提交给事件循环,不需要等待
    task_a = asyncio.create_task(task("A", 2))
    task_b = asyncio.create_task(task("B", 1))

    # 此时 A 和 B 已经在并发运行
    result_a = await task_a
    result_b = await task_b
    print(result_a, result_b)

asyncio.TaskGroup — Python 3.11+(推荐)

async def main():
    async with asyncio.TaskGroup() as tg:
        task_a = tg.create_task(task("A", 1))
        task_b = tg.create_task(task("B", 2))
    # 退出 with 块时自动等待所有任务完成
    # 任意一个任务失败,其余任务会被取消,并抛出 ExceptionGroup
    print(task_a.result(), task_b.result())

asyncio.wait() — 更细粒度的控制

import asyncio

async def main():
    tasks = [asyncio.create_task(task(f"T{i}", i)) for i in range(1, 4)]

    # 等待第一个完成就返回
    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)

    for t in done:
        print(t.result())

    # 取消未完成的任务
    for t in pending:
        t.cancel()

return_when 可选值:

说明
FIRST_COMPLETED 第一个任务完成时返回
FIRST_EXCEPTION 第一个任务抛出异常时返回
ALL_COMPLETED 所有任务完成时返回(默认)

4. 超时控制

asyncio.timeout() — Python 3.11+(推荐)

async def main():
    try:
        async with asyncio.timeout(3.0):  # 3 秒内未完成则取消
            result = await slow_operation()
    except TimeoutError:
        print("操作超时")

asyncio.wait_for() — 兼容旧版本

async def main():
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=3.0)
    except asyncio.TimeoutError:
        print("操作超时")

5. 同步原语

Lock — 互斥锁

import asyncio

lock = asyncio.Lock()
shared_resource = []

async def write(value: int):
    async with lock:  # 同一时刻只允许一个协程进入
        shared_resource.append(value)
        await asyncio.sleep(0.01)  # 模拟写入耗时

Semaphore — 控制并发数

import asyncio
import httpx

sem = asyncio.Semaphore(10)  # 最多同时 10 个并发请求

async def fetch(client: httpx.AsyncClient, url: str) -> str:
    async with sem:
        response = await client.get(url)
        return response.text

async def main(urls: list[str]):
    async with httpx.AsyncClient() as client:
        tasks = [fetch(client, url) for url in urls]
        results = await asyncio.gather(*tasks)
    return results

Event — 事件通知

import asyncio

event = asyncio.Event()

async def producer():
    await asyncio.sleep(2)
    print("数据准备好了")
    event.set()  # 通知等待者

async def consumer():
    await event.wait()  # 阻塞直到 event 被 set
    print("开始消费数据")

async def main():
    await asyncio.gather(producer(), consumer())

Queue — 生产者消费者

import asyncio

async def producer(queue: asyncio.Queue):
    for i in range(5):
        await queue.put(i)
        print(f"生产 {i}")
        await asyncio.sleep(0.5)
    await queue.put(None)  # 发送结束信号

async def consumer(queue: asyncio.Queue):
    while True:
        item = await queue.get()
        if item is None:
            break
        print(f"消费 {item}")
        queue.task_done()  # 标记任务完成

async def main():
    queue = asyncio.Queue(maxsize=3)  # 最多缓存 3 个
    await asyncio.gather(producer(queue), consumer(queue))

6. 在线程/进程中运行阻塞代码

异步代码中调用同步阻塞函数(如读文件、调用同步库)会阻塞整个事件循环。应用 run_in_executor 放到线程池/进程池执行:

import asyncio
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def blocking_io(n: int) -> int:
    time.sleep(1)  # 阻塞操作
    return n * 2

def cpu_bound(n: int) -> int:
    return sum(range(n))  # CPU 密集

async def main():
    loop = asyncio.get_event_loop()

    # 线程池(适合 I/O 阻塞)
    with ThreadPoolExecutor(max_workers=4) as pool:
        result = await loop.run_in_executor(pool, blocking_io, 10)
        print(result)  # 20

    # 进程池(适合 CPU 密集)
    with ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, cpu_bound, 10_000_000)
        print(result)

asyncio.to_thread() — Python 3.9+(更简洁)

import asyncio

async def main():
    result = await asyncio.to_thread(blocking_io, 10)
    print(result)

7. 异步上下文管理器与迭代器

异步上下文管理器

class AsyncDBConnection:
    async def __aenter__(self):
        self.conn = await connect_db()
        return self.conn

    async def __aexit__(self, *args):
        await self.conn.close()

async def main():
    async with AsyncDBConnection() as conn:
        await conn.execute("SELECT 1")

异步迭代器

class AsyncRange:
    def __init__(self, stop: int):
        self.stop = stop
        self.current = 0

    def __aiter__(self):
        return self

    async def __anext__(self):
        if self.current >= self.stop:
            raise StopAsyncIteration
        await asyncio.sleep(0.1)
        value = self.current
        self.current += 1
        return value

async def main():
    async for i in AsyncRange(5):
        print(i)

8. 常用代码段

限速并发请求

import asyncio
import httpx
from typing import Any

async def fetch_all(urls: list[str], concurrency: int = 10) -> list[Any]:
    sem = asyncio.Semaphore(concurrency)

    async def fetch(client: httpx.AsyncClient, url: str):
        async with sem:
            r = await client.get(url, timeout=10)
            r.raise_for_status()
            return r.json()

    async with httpx.AsyncClient() as client:
        return await asyncio.gather(*[fetch(client, u) for u in urls])

带重试的异步任务

import asyncio

async def with_retry(coro_fn, *args, retries: int = 3, delay: float = 1.0):
    for attempt in range(retries):
        try:
            return await coro_fn(*args)
        except Exception as e:
            if attempt == retries - 1:
                raise
            await asyncio.sleep(delay * (2 ** attempt))  # 指数退避

异步定时任务

import asyncio

async def periodic(interval: float):
    while True:
        await do_work()
        await asyncio.sleep(interval)

async def main():
    task = asyncio.create_task(periodic(60))
    # 运行主程序...
    await asyncio.sleep(3600)
    task.cancel()

超时 + 重试组合

import asyncio

async def fetch_with_timeout_retry(url: str, timeout: float = 5.0, retries: int = 3):
    for attempt in range(retries):
        try:
            async with asyncio.timeout(timeout):
                return await fetch(url)
        except (TimeoutError, Exception) as e:
            if attempt == retries - 1:
                raise
            await asyncio.sleep(2 ** attempt)

9. 最佳实践

只在顶层调用 asyncio.run()

# 正确:顶层入口调用一次
asyncio.run(main())

# 错误:在协程内部调用 asyncio.run()(会嵌套事件循环)
async def bad():
    asyncio.run(some_coro())  # 报错

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

import time

# 错误:直接调用阻塞函数,会卡住整个事件循环
async def bad():
    time.sleep(1)       # 阻塞!
    requests.get(url)   # 阻塞!

# 正确
async def good():
    await asyncio.to_thread(time.sleep, 1)
    await httpx.AsyncClient().get(url)  # 使用异步 HTTP 库

用 TaskGroup 代替裸 gather(Python 3.11+)

TaskGroup 在任意子任务失败时会自动取消其余任务,并将所有异常汇总为 ExceptionGroup,比 gather 行为更安全可预期。

合理设置并发上限

不要无限制地 gather 大量任务,始终通过 Semaphore 限制并发数,避免连接池耗尽或目标服务被压垮。


10. 踩坑与注意事项

协程对象未被 await 会静默丢弃

async def main():
    fetch_data("A", 1)  # 没有 await,协程对象被创建但从未执行
    # Python 会警告:RuntimeWarning: coroutine 'fetch_data' was never awaited

asyncio.sleep(0) 主动让出控制权

在长时间 CPU 运算中,插入 await asyncio.sleep(0) 可以让事件循环有机会处理其他事件:

async def heavy_work(items):
    for i, item in enumerate(items):
        process(item)
        if i % 100 == 0:
            await asyncio.sleep(0)  # 每处理 100 个让出一次

不能在非异步上下文中直接 await

# 普通函数中无法使用 await
def sync_func():
    result = await some_coro()  # SyntaxError

# 如果必须在同步代码中调用协程
result = asyncio.run(some_coro())      # 如果当前没有运行中的事件循环
result = asyncio.get_event_loop().run_until_complete(some_coro())  # 旧写法

CancelledError 不能被吞掉

# 错误:捕获了 CancelledError 却没有重新抛出,导致任务无法被取消
async def bad():
    try:
        await asyncio.sleep(10)
    except Exception:  # 意外捕获了 CancelledError
        pass

# 正确:单独处理 CancelledError
async def good():
    try:
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        # 做清理工作
        raise  # 必须重新抛出
    except Exception as e:
        handle(e)

最佳实践

asyncio.gather vs asyncio.TaskGroup:Python 3.11+ 优先用 TaskGroup,其中任一任务失败时会取消其余任务,比 gather 的默认行为(继续运行其他任务)更安全:

async with asyncio.TaskGroup() as tg:
    t1 = tg.create_task(fetch(url1))
    t2 = tg.create_task(fetch(url2))
# 任一失败则两个都取消

控制并发量用 asyncio.Semaphore:批量请求时不设上限会耗尽连接池或触发限流,用 Semaphore 限制同时进行的协程数:

sem = asyncio.Semaphore(10)
async def limited_fetch(url):
    async with sem:
        return await client.get(url)

避免在协程中混用 asyncio.sleep(0) 作为让步await asyncio.sleep(0) 让出控制权,但过于频繁会降低吞吐量,真正需要让步的场景是长计算循环,而非 IO 等待(IO 本身已让步)。

asyncio.wait_for 设置单个协程超时:比在客户端设置全局超时更精确:

try:
    result = await asyncio.wait_for(coro(), timeout=5.0)
except asyncio.TimeoutError:
    handle_timeout()

loop.run_in_executor 桥接同步阻塞代码:无异步版本的库(boto3、同步 DB 驱动)通过线程池运行,不阻塞事件循环:

result = await loop.run_in_executor(None, sync_blocking_call, arg1)

常见陷阱

陷阱:asyncio.create_task 的任务被 GC 回收

现象: 创建的后台任务有时无征兆地停止,日志中可能出现 Task was destroyed but it is pending!
原因: create_task 返回的 Task 对象若没有被引用,可能被垃圾回收器回收,导致任务取消。
解决: 将任务保存到集合中,任务完成后移除:

background_tasks = set()
task = asyncio.create_task(coro())
background_tasks.add(task)
task.add_done_callback(background_tasks.discard)

陷阱:在同步上下文中调用 asyncio.run 嵌套事件循环

现象: 在已有事件循环(如 Jupyter、FastAPI 路由)中调用 asyncio.run(coro())This event loop is already running
原因: asyncio.run 创建新事件循环,但当前线程已有运行中的循环,不允许嵌套。
解决: 在已有循环的上下文中用 await coro() 直接调用;Jupyter 中可用 nest_asyncio.apply()

陷阱:shield 不能阻止外部取消

现象:asyncio.shield(coro()) 期望保护协程不被取消,但协程仍然被取消了。
原因: shield 只保护内部协程不因外层 Future 取消而停止,但若持有 shield Future 的任务本身被取消,shield 也会被取消。
解决: 真正不可中断的清理操作用独立 Task + shield,并用 try/finally 确保执行完毕。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog