导航菜单

  • 1.什么是MCP
  • 2.MCP架构
  • 3.MCP服务器
  • 4.MCP客户端
  • 5.版本控制
  • 6.连接MCP服务器
  • 7.SDKs
  • 8.Inspector
  • 9.规范
  • 10.架构
  • 11.协议
  • 12.生命周期
  • 13.工具
  • 14.资源
  • 15.提示
  • 16.日志
  • 17.进度
  • 18.传输
  • 19.补全
  • 20.引导
  • 21.采样
  • 22.任务
  • 23.取消
  • 24.Ping
  • 25.根
  • 26.分页
  • 27.授权
  • 28.初始化
  • 29.工具
  • 30.资源
  • 31.结构化输出
  • 32.提示词
  • 33.上下文
  • 34.StreamableHTTP
  • 35.参数补全
  • 36.引导
  • 37.采样
  • 38.LowLevel
  • 39.任务
  • 40.取消
  • 41.ping
  • 42.根
  • 43.分页
  • 44.授权
  • 45.授权
  • Keycloak
  • asyncio
  • contextlib
  • httpx
  • pathlib
  • pydantic
  • queue
  • starlette
  • subprocess
  • threading
  • uvicorn
  • JSON-RPC
  • z
  • 1. 什么是 I/O 等待?
    • 1.1 什么是 I/O 等待?
    • 1.2 同步 vs 异步
    • 1.3 为什么需要 asyncio?
  • 2. 什么是 asyncio?
  • 3. 协程:async def 和 await
    • 3.1 定义协程
    • 3.2 在协程里调用另一个协程
  • 4. 并发:create_task 和 gather
    • 4.1 用 create_task 并发运行
    • 4.2 用 gather 批量并发
  • 5. 事件循环
  • 6. 模拟多个网络请求
  • 7. 注意事项
    • 7.1 不要用同步阻塞代码
    • 7.2 必须用异步库
    • 7.3 协程必须由事件循环运行
  • 8. asyncio 与多线程对比
  • 9. 总结

1. 什么是 I/O 等待? #

1.1 什么是 I/O 等待? #

程序经常需要等待某些操作完成,例如:

  • 等网络请求返回
  • 等磁盘读写完成
  • 等数据库查询结果

这类操作叫 I/O 操作。在等待期间,CPU 大部分时间在"干等",没有做有用的事。

1.2 同步 vs 异步 #

同步:代码一行行执行,遇到 I/O 就停在那里等,等完再继续。后面的代码必须等前面的 I/O 完成。

异步:遇到 I/O 时,不傻等,而是先去做别的事,等 I/O 完成后再回来处理。这样多个 I/O 可以"重叠"进行,提高效率。

通俗比喻:同步像一个人排队等奶茶,等的时候一直站着;异步像排队时顺便回消息、刷手机,奶茶好了再过去取。

1.3 为什么需要 asyncio? #

  • 多线程:可以并发,但线程切换有开销,共享数据要加锁,容易出错。
  • asyncio:在单线程里用协程实现并发,没有线程切换,一般不需要锁,代码更简洁。

适用场景:I/O 密集型(网络、文件、数据库)。不适用:CPU 密集型(大量计算),应改用多进程。

2. 什么是 asyncio? #

asyncio 是 Python 3.4+ 引入的标准库,用于编写异步并发代码,核心是:

  • 协程:用 async def 定义的函数,用 await 挂起等待
  • 事件循环:负责调度协程,当一个协程等待时,运行其他协程

3. 协程:async def 和 await #

3.1 定义协程 #

用 async def 定义的函数叫协程函数。调用它不会立即执行,而是返回一个协程对象。

# 导入 asyncio
import asyncio

# 用 async def 定义协程函数
async def say_hello():
    # 打印第一行
    print("Hello")
    # 等待 1 秒(模拟 I/O),不阻塞其他协程
    await asyncio.sleep(1)
    # 打印第二行
    print("World")

# 协程函数必须由事件循环运行,不能直接调用
# asyncio.run() 会创建事件循环并运行传入的协程
asyncio.run(say_hello())

说明:await asyncio.sleep(1) 表示"挂起 1 秒",在这 1 秒内事件循环可以运行其他协程。asyncio.sleep 是异步的,不会阻塞。

3.2 在协程里调用另一个协程 #

协程内部用 await 调用其他协程,等待其完成后再继续。

# 导入 asyncio
import asyncio

# 定义协程:等待 2 秒后返回
async def slow_task():
    await asyncio.sleep(2)
    return "完成"

# 定义主协程
async def main():
    # 先打印
    print("开始")
    # 等待 slow_task 完成,拿到返回值
    result = await slow_task()
    # 打印返回值
    print("结果:", result)

# 运行
asyncio.run(main())

4. 并发:create_task 和 gather #

4.1 用 create_task 并发运行 #

asyncio.create_task() 把协程包装成任务,放入事件循环立即开始执行,不阻塞当前协程。

# 导入 asyncio
import asyncio

# 模拟一个耗时 1 秒的任务
async def task(name):
    print(f"{name} 开始")
    await asyncio.sleep(1)
    print(f"{name} 结束")

# 主协程
async def main():
    # 创建两个任务,立即开始执行(不等待)
    t1 = asyncio.create_task(task("任务A"))
    t2 = asyncio.create_task(task("任务B"))
    # 等待两个任务都完成
    await t1
    await t2
    print("全部完成")

# 运行
asyncio.run(main())

输出顺序:先打印 "任务A 开始" 和 "任务B 开始",约 1 秒后打印 "任务A 结束" 和 "任务B 结束",两个任务并发执行。

4.2 用 gather 批量并发 #

asyncio.gather() 可以同时运行多个协程,并等待全部完成,返回结果列表。

# 导入 asyncio
import asyncio

# 模拟耗时任务,返回结果
async def fetch(id):
    await asyncio.sleep(1)
    return f"结果-{id}"

# 主协程
async def main():
    # 并发运行 3 个 fetch,返回结果列表
    results = await asyncio.gather(
        fetch(1),
        fetch(2),
        fetch(3),
    )
    # 打印所有结果
    print(results)

# 运行
asyncio.run(main())

说明:gather 会并发执行 3 个 fetch,总耗时约 1 秒(而非 3 秒),因为它们是同时进行的。

5. 事件循环 #

事件循环是 asyncio 的"调度器",负责:

  1. 运行协程
  2. 当协程 await 时,切换到其他可运行的协程
  3. 当 I/O 完成时,唤醒等待的协程

通常不需要手动操作事件循环,asyncio.run() 会自动创建并运行。

6. 模拟多个网络请求 #

下面用 asyncio.sleep 模拟多个"网络请求",演示并发效果。

# 导入 asyncio
import asyncio
import time

# 模拟一次网络请求,耗时 1 秒
async def fetch_url(url_id):
    print(f"请求 {url_id} 开始")
    # 模拟网络延迟
    await asyncio.sleep(1)
    print(f"请求 {url_id} 完成")
    return f"url_{url_id}"

# 主协程
async def main():
    # 记录开始时间
    start = time.perf_counter()
    # 并发发起 5 个"请求"
    results = await asyncio.gather(
        fetch_url(1),
        fetch_url(2),
        fetch_url(3),
        fetch_url(4),
        fetch_url(5),
    )
    # 计算总耗时
    elapsed = time.perf_counter() - start
    print(f"全部完成,耗时 {elapsed:.1f} 秒")
    print("结果:", results)

# 运行
asyncio.run(main())

说明:5 个请求并发执行,总耗时约 1 秒;若串行执行,则需要 5 秒。

7. 注意事项 #

7.1 不要用同步阻塞代码 #

在协程里不要调用会阻塞的同步函数(如 time.sleep、requests.get),否则会阻塞整个事件循环,其他协程无法运行。

# 错误示例:不要这样做!
import asyncio
import time

async def bad():
    time.sleep(1)  # 错误!会阻塞事件循环

# 正确做法:用 asyncio.sleep
async def good():
    await asyncio.sleep(1)  # 正确,不会阻塞

7.2 必须用异步库 #

网络请求要用 aiohttp 等异步库,不能用 requests(同步)。文件读写要用 aiofiles 等,不能用普通的 open。否则会阻塞事件循环。

7.3 协程必须由事件循环运行 #

协程不能直接调用,必须通过 asyncio.run() 或 await 来执行。在顶层入口用 asyncio.run(main())。

8. asyncio 与多线程对比 #

特性 asyncio 多线程
并发模型 单线程 + 事件循环 多线程
切换开销 极小 较大
数据共享 一般无需锁 需锁
适用场景 I/O 密集型 I/O 密集型
编程难度 需理解 async/await 需处理线程安全

9. 总结 #

内容 要点
定义协程 async def
等待协程 await
运行入口 asyncio.run(main())
并发执行 create_task() 或 gather()
模拟等待 await asyncio.sleep(秒数)

记忆口诀:协程用 async def,等待用 await,入口用 asyncio.run,并发用 gather。

← 上一节 45.授权 下一节 contextlib →

访问验证

请输入访问令牌

Token不正确,请重新输入