Day 08 / 共 68 天 · 阶段 1 编程与工具地基

异步与并发入门

昨天(Day 07)你会调 API 了,但每个请求都要干等服务器回话。今天解决"等"的问题——用 async/await 让多个请求几乎同时进行,把"排队等 10 次"变成"一起等 1 次"。明天(Day 09)你会把自己的功能做成一个 API 服务,那正是异步大显身手的地方。

📍 你在 68 天里的位置(阶段 1:编程与工具地基 · Day 3-9)
D03 Python 基础 D06 命令行&Git D07 调 API D08 异步 D09 FastAPI
💡 先用一个类比兜住今天(今天世界观:一个服务员招呼多桌客人) 想象一家餐厅只有一个服务员同步做法:他招呼完第 1 桌、盯着后厨把第 1 桌的菜做完端上,才去招呼第 2 桌——后厨做菜的几分钟里他干站着,白白浪费。异步做法:他给第 1 桌下完单,不等菜好就转身去招呼第 2、3 桌,哪桌菜好了就去端哪桌。人还是那一个(单线程),但因为"等菜的时间被用来干别的活",整体快了好几倍。今天记住:"异步不是变出更多人手,而是不让人干等",全通。
L01

痛点:串着等,一个 Agent 慢到想砸键盘

🤔 痛点假设你的 Agent 要同时查天气、查股价、查新闻三个 API,每个各花 1 秒。昨天的写法是一个接一个调,总共要 3 秒;如果是 10 个 API 就是 10 秒——用户早跑了。
💡 本质这 3 秒里,你的程序其实99% 时间在干等网络,CPU 闲着。异步的目标就是:把这些"等"重叠起来同时进行,让 3 个 1 秒的等待挤在同 1 秒里完成。

这类"大部分时间在等外部(网络、磁盘、数据库)"的任务,叫 I/O 密集型。Agent 干的活几乎全是这种——调大模型、查向量库、访问工具 API,无一不是"发出去、等回来"。所以异步对 Agent不是锦上添花,是刚需

👶 一句话“串着等”= 排队打饭,前一个人打完你才能动;“异步并发”= 大家同时把餐盘递进窗口,一起等菜。
L02

同步 vs 异步:一张时间线看懂

同一个服务员招呼 3 桌,两种干法的时间线对比:

3 个各 1 秒的请求:同步 3 秒 vs 异步 ~1 秒 同步 请求1(等1秒) 请求2(等1秒) 请求3(等1秒) = 3 秒 异步 请求1(等1秒) 请求2(等1秒) 请求3(等1秒) ≈ 1 秒(三个等待重叠) 0s1s2s3s 关键:异步没有更多"人手",只是把三段"干等"叠在了一起
图注:同步是"等完一个再下一个",异步是"三个一起等"。省的正是那段"干等网络"的时间。
 同步(sync)异步(async)
做事方式一件做完再做下一件发起后不干等,去做别的,回头收结果
适合纯计算(CPU 忙个不停)大量"等外部"的 I/O 任务
3 个 1 秒请求≈ 3 秒≈ 1 秒
澄清一个误解:异步和"多线程/多进程"不是一回事。异步还是一个人(单线程),只是这个人很会"见缝插针";它专治"等",不能让纯计算变快。
L03

async / await:两个关键字就够用

💡 本质Python 用两个关键字表达异步:async def 定义一个"可以中途让出的函数"(叫协程),await 表示"这里要等一下,等的时候把控制权让出去,让别人先干活"。

await 就是服务员说"这桌的菜我先挂着等,趁这空儿我去招呼别桌"的那个动作:

import asyncio                        # Python 自带的异步库

async def fetch_weather(city):        # async def = 这是一个协程(可让出的函数)
    print(f"开始查 {city} 天气…")
    await asyncio.sleep(1)            # await = 等 1 秒(模拟网络),等的时候让出控制权
    print(f"{city} 天气拿到了")
    return f"{city}: 晴 28℃"          # 和普通函数一样能 return
两条铁律:① await 只能写在 async def 里面;② 调用一个 async 函数时前面必须加 await(或交给下节的工具去跑),否则它根本不会执行,只得到一个"协程对象"——这是新手最常见的坑。

👶 小白:为什么调 fetch_weather("北京") 没加 await,程序不报错也不干活?

👨‍🏫 老师:因为 async 函数被调用时只是造出一份"待办任务单"(协程对象),并不立刻开工。await 才是"把这张单子交出去、真正执行并等它完成"的动作。你可以把 async def 想成"写好了菜谱",await 才是"下令开火做这道菜"。菜谱写了不做,当然桌上没菜。

L04

用 asyncio.run 把异步"启动"起来

🤔 痛点await 只能在 async def 里用,但程序最外层的入口是普通代码——那第一个 async 函数由谁来"启动"?
💡 本质asyncio.run(...)。它就是"开门营业"的开关:启动一个后台调度器(事件循环),把你的顶层协程跑到结束,再关门。整个程序通常只在最外层调用它一次。
import asyncio

async def main():                     # 通常把顶层逻辑放进一个叫 main 的协程
    result = await fetch_weather("北京")   # 这里能用 await,因为在 async def 里
    print("结果:", result)

asyncio.run(main())                   # ← 唯一的“启动”入口,从普通代码进入异步世界
# 输出:
# 开始查 北京 天气…
# (等约1秒)
# 北京 天气拿到了
# 结果: 北京: 晴 28℃
记住这个结构async def main(): ... 写逻辑,最后一行 asyncio.run(main()) 点火。几乎所有异步 Python 脚本都长这样。
L05

asyncio.gather:让它们真正"一起等"

🤔 痛点上面只查了一个城市还是 1 秒。可如果我 await 三次——await a; await b; await c——那不还是一个等完再等下一个,又变回 3 秒了吗?
💡 本质没错,连续 await 依然是串行。要真正并发,用 asyncio.gather(...):把多个协程一次性全部发起,然后统一等它们一起完成。这才是服务员"同时给三桌下单"。
import asyncio, time

async def fetch_weather(city):
    await asyncio.sleep(1)            # 每个都“等”1 秒
    return f"{city}: 晴"

async def main():
    t = time.time()
    # gather 把 3 个协程同时发起,一起等 → 总共约 1 秒而不是 3 秒
    results = await asyncio.gather(
        fetch_weather("北京"),
        fetch_weather("上海"),
        fetch_weather("广州"),
    )
    print(results)                    # ['北京: 晴', '上海: 晴', '广州: 晴'](顺序和传入一致)
    print(f"耗时 {time.time()-t:.1f} 秒")   # 耗时 1.0 秒 ← 三个等待重叠了!

asyncio.run(main())
📝 例子:数字会说话 串行 await 三次:1 + 1 + 1 = 3.0 秒
asyncio.gather 三个一起:max(1, 1, 1) = 1.0 秒
请求越多差距越夸张:100 个各 1 秒的请求,串行要 100 秒,gather 仍约 1 秒。这就是异步的威力。
小贴士gather 返回的结果列表顺序和你传入的顺序一致,不会因为谁先完成就乱序,取值很安心。
L06

为什么 Agent 离不开异步

把今天的知识对到 Agent 的真实场景,你会发现异步无处不在:

  • 并行取证:一个"研究 Agent"要同时查 5 个资料源,gather 一起查,5 秒变 1 秒;
  • 多智能体协作:主管同时派 3 个专家 Agent 干活,它们并发跑,最后汇总(后面阶段 8 会讲);
  • 流式输出:大模型一个字一个字往外吐,你要一边收一边处理,天生就是异步的活(明天/Day 12 会用到);
  • 高并发服务:你的 Agent 做成 API 后要同时接待很多用户——异步能用一个进程扛住大量"都在等模型回话"的请求。
💡 本质Agent = 大量"发出去、等回来"的编排。谁在"等"的时间里还能干别的活,谁就快、就省钱、就能扛并发。所以 LangGraph、FastAPI 这些你后面要学的框架,核心 API 全是 async 的——今天这一课是它们的入场券。
L07

三个新手大坑(提前躲开)

现象正确姿势
① 忘了 await函数"没执行",得到 <coroutine object> 警告调用 async 函数前一定加 await
② 在异步里用了"阻塞"调用time.sleep() 或昨天的 requests,整个程序还是卡死睡用 asyncio.sleep();HTTP 用异步库 httpx/aiohttp
③ 连续 await 当并发以为快了,其实还是串行要并发就用 asyncio.gather()

第②坑最隐蔽也最重要:异步世界里混进一个"阻塞"调用,会把整条流水线全堵死——就像服务员本该去招呼别桌,却站在后厨前发呆。所以昨天的同步 requests 在异步代码里要换成它的异步兄弟 httpx

import httpx                          # pip install httpx(requests 的异步版)

async def get_json(url):
    async with httpx.AsyncClient() as client:   # async with = 异步版的 with
        resp = await client.get(url, timeout=10)  # await 发请求,等的时候让出控制权
        return resp.json()

# 之后就能 await asyncio.gather(get_json(u1), get_json(u2), ...) 真正并发拉取

👶 小白:那我昨天刚学的 requests 白学了?

👨‍🏫 老师:完全没有!写小脚本、一次只调一个接口时,同步 requests 又简单又够用,仍是最常用的库。只有当你要同时发很多请求、或身处 async 框架(FastAPI/LangGraph)里时,才换 httpx。两者 API 几乎一模一样,你昨天的功夫直接迁移过来,只是多加了 async/await

L08

今日小结 + 动手 10 分钟

🧠 今天你应该能回答

  • 异步到底省了什么时间?(省"干等外部 I/O"的时间,把多段等待重叠)
  • 异步和多线程一样吗?(不一样,异步还是单线程,只是不干等;专治 I/O,不加速纯计算)
  • async / await 各是什么?(定义可让出的协程 / 标记"这里等一下并让出控制权")
  • 怎么启动?怎么真正并发?(asyncio.run(main()) 启动;asyncio.gather(...) 并发)
  • 为什么 Agent 离不开它?(Agent 全是"发出去等回来"的 I/O,谁不干等谁就快)

✋ 动手:亲手把 3 秒压成 1 秒

# 无需任何第三方库,asyncio 是 Python 自带的
python async_demo.py
# 存成 async_demo.py
import asyncio, time

async def task(name, seconds):
    print(f"{name} 开始…")
    await asyncio.sleep(seconds)      # 模拟等网络
    print(f"{name} 完成")
    return name

async def main():
    # —— 对照 1:串行(连续 await),看它花约 3 秒 ——
    t = time.time()
    await task("串行A", 1); await task("串行B", 1); await task("串行C", 1)
    print(f"串行耗时 {time.time()-t:.1f} 秒\n")

    # —— 对照 2:并发(gather),看它只花约 1 秒 ——
    t = time.time()
    await asyncio.gather(task("并发A", 1), task("并发B", 1), task("并发C", 1))
    print(f"并发耗时 {time.time()-t:.1f} 秒")

asyncio.run(main())

进阶挑战:把 gather 里的任务改成 5 个、每个睡 2 秒,先猜总耗时,再运行验证。(答案:约 2 秒,因为 5 个 2 秒的等待全重叠了。)

明天预告 · Day 09 FastAPI:前两天你一直在"调别人的 API"。明天反过来——用 FastAPI 把你自己的功能做成一个 API 服务,别人也能调你。你会用到今天的 async,写 @app.get/@app.post,用 pydantic 校验数据,还能白拿一个自动生成的接口文档页 /docs。这是把 Agent 交付上线的第一步。
← Day 07 调第一个 API Day 09 · FastAPI 做一个服务 →