1. 同步
程序由一个个任务组成,例如读取文件、请求模型、查询数据库和计算结果。理解同步与异步,关键不是看代码有没有多个函数,而是看调用方在等待结果时能不能处理其他工作。
同步(synchronous)表示调用方发起一个操作后,要等这个操作完成并返回结果,才能继续执行下一步:
01import time0203def fetch(name):04print(f"{name}:开始请求")05time.sleep(2)06print(f"{name}:请求完成")07return f"{name} 的结果"0809first = fetch("任务一")10second = fetch("任务二")1112print(first)13print(second)
执行过程是:
- 调用
fetch("任务一"); - 等待任务一完成;
- 得到
first; - 调用
fetch("任务二"); - 等待任务二完成;
- 得到
second。
两个任务各等待 2 秒,总耗时大约是 4 秒。这种执行方式也叫串行:上一个任务没有结束,下一个任务就不会开始。
示例中的 time.sleep() 会阻塞当前线程。严格来说,「同步」描述的是调用方如何取得结果,「阻塞」描述的是等待期间当前线程是否还能执行其他工作。它们不是完全相同的概念,不过在这种常见的同步调用中,两者通常同时出现。
同步并不是落后的写法。任务之间存在先后依赖、程序规模很小,或者底层库只提供同步 API 时,同步代码通常更直接,也更容易调试。
2. 异步
异步(asynchronous)表示任务等待结果时可以暂停,把执行权交还给调度器。调度器可以利用这段等待时间运行其他已经就绪的任务,结果准备好后再恢复原来的任务。
Python 主要通过标准库 asyncio 提供这种能力:
01import asyncio0203async def fetch(name):04print(f"{name}:开始请求")05await asyncio.sleep(2)06print(f"{name}:请求完成")07return f"{name} 的结果"0809async def main():10result = await fetch("任务一")11print(result)1213asyncio.run(main())
这里出现了三个重要语法:
async def定义协程函数;await等待异步操作,并允许当前协程暂停;asyncio.run()创建并管理事件循环,运行入口协程。
执行到 await asyncio.sleep(2) 时,fetch() 暂时不能继续,但它不会像 time.sleep(2) 那样占住事件循环。事件循环可以去运行其他已经准备好的任务。两秒后,fetch() 获得继续执行的资格,再从 await 后面恢复。
异步也不是「把函数扔到后台就不管了」。调用方最终仍然要通过 await 等方式取得结果、处理异常和取消任务。异步改变的是等待方式,不会改变业务上的依赖关系。
如果你熟悉 JavaScript,可以通过下面的表格建立初步对应关系:
| JavaScript | Python asyncio | 作用与区别 |
|---|---|---|
| Event Loop | asyncio 事件循环 | 负责调度异步任务和 IO 事件 |
async function | async def | 定义异步函数;Python 调用后先得到协程对象,不会立即执行函数体 |
await | await | 暂停当前异步任务,等待结果完成后恢复 |
Promise | 协程、Task、Future | 都与未来结果有关,但职责不同,并不是一一对应关系 |
Promise.all() | asyncio.gather()、TaskGroup | 并发等待一组异步任务 |
setTimeout() | asyncio.sleep() | 延迟一段时间;sleep() 暂停协程,不接收回调函数 |
setTimeout(callback) | loop.call_later() | 在指定延迟后调度普通回调 |
Promise.then()、queueMicrotask() | 没有直接对应的公开微任务 API | asyncio 使用自己的就绪队列、Task 和 Future 调度模型 |
这张表只用于帮助迁移已有认知,不能把两套运行时机械地画等号。尤其是 JavaScript 调用 async function 会返回 Promise,而 Python 调用 async def 函数只会创建协程对象;协程还需要被 await 或包装成 Task,才会真正开始执行。
JavaScript 通常用「宏任务、微任务」解释回调顺序,asyncio 则更适合用「协程、Task、Future、就绪队列、IO 事件和定时任务」理解。后面的「事件循环」文章会继续展开这些概念。
3. 并发
并发(concurrency)表示一段时间内有多个任务都在推进。它强调的是任务之间可以交替执行,而不是一定要在同一时刻运行。
只把函数写成 async def,不会自动产生并发。下面虽然使用了异步语法,两个任务仍然是串行的:
01import asyncio02import time0304async def fetch(name):05print(f"{name}:开始")06await asyncio.sleep(2)07print(f"{name}:完成")0809async def main():10started_at = time.perf_counter()1112await fetch("任务一")13await fetch("任务二")1415elapsed = time.perf_counter() - started_at16print(f"耗时约 {elapsed:.1f} 秒")1718asyncio.run(main())
第一个 await fetch(...) 完成后,程序才会执行第二个调用,因此总耗时仍然接近 4 秒。await 表示等待,不表示自动同时启动后面的任务。
要形成并发,需要让多个协程在同一段时间内都进入事件循环。可以使用 asyncio.gather():
01import asyncio02import time0304async def fetch(name):05print(f"{name}:开始")06await asyncio.sleep(2)07print(f"{name}:完成")08return name0910async def main():11started_at = time.perf_counter()1213results = await asyncio.gather(14fetch("任务一"),15fetch("任务二"),16)1718elapsed = time.perf_counter() - started_at19print(results)20print(f"耗时约 {elapsed:.1f} 秒")2122asyncio.run(main())
任务一执行到 await 后暂停,事件循环开始运行任务二;任务二也进入等待后,事件循环继续监听它们。大约两秒后,两个任务先后恢复并完成,所以总耗时接近 2 秒。
因此,「异步」和「并发」不是同义词:异步提供可暂停、可恢复的等待方式,并发则是利用这种能力,让多个任务在同一段时间内推进。
并发也不只属于 asyncio。多线程、多进程和分布式服务同样可以让多个任务在一段时间内推进;asyncio 只是 Python 处理大量 IO 等待时的一种高效并发方式。
4. 并行
并行(parallelism)表示多个任务在同一时刻真正执行,通常需要多个 CPU 核心、多个进程,或者能够并行工作的底层程序。
同步、异步、并发和并行描述的不是同一个维度:
| 概念 | 关注的问题 | 常见表现 |
|---|---|---|
| 同步 | 调用方是否要立即等待结果 | 得到结果后才继续下一步 |
| 异步 | 等待期间能否让出执行权 | 在 await 处暂停,稍后恢复 |
| 并发 | 一段时间内是否有多个任务在推进 | 多个任务交替执行或共同等待 |
| 并行 | 同一时刻是否真的执行多个任务 | 多个核心或进程同时计算 |
单线程事件循环可以实现并发,但不能让两段纯 Python 代码在同一个线程上并行执行:
01import asyncio0203async def calculate():04total = 005for number in range(20_000_000):06total += number07return total0809async def main():10await asyncio.gather(calculate(), calculate())1112asyncio.run(main())
calculate() 内部没有 await。第一个任务开始计算后不会主动让出执行权,第二个任务只能等待,因此使用 gather() 也不会使计算变快,反而可能阻塞整个事件循环。
真正需要并行执行 CPU 密集型任务时,通常会使用多个进程、任务队列或独立的计算服务。线程是否能够提升 CPU 计算效率,还会受到 Python 实现和 GIL 等因素影响,后面的「线程」文章会专门说明。
5. 场景
asyncio 的优势来自「等待期间还有其他工作可以做」,因此最适合 IO 密集型任务:
- 请求大模型或其他 HTTP 服务;
- 查询数据库、Redis 和向量数据库;
- 等待消息队列;
- 读取网络数据流;
- 同时维护大量连接。
CPU 密集型任务则会持续使用处理器,例如:
- 大量数值计算;
- 图片和视频处理;
- 本地模型推理;
- 大规模文本解析和压缩。
可以用下面的判断方式选择执行模型:
| 任务 | 常见选择 |
|---|---|
| 简单流程或有严格先后依赖 | 同步或异步串行 |
| 大量相互独立的 IO 等待 | asyncio 并发 |
| 无法替换的同步阻塞 IO | 线程池或 asyncio.to_thread() |
| CPU 密集型计算 | 多进程、任务队列或计算服务 |
异步并不一定比同步更快。只有当程序存在可重叠的等待时间时,并发执行才能提高整体吞吐量。单个异步请求的耗时通常不会凭空缩短,改进的是等待期间处理其他请求的能力。
6. 服务
在 FastAPI 服务中,服务器已经创建并维护事件循环:
01import asyncio02from fastapi import FastAPI0304app = FastAPI()0506async def call_model():07await asyncio.sleep(1) # 模拟等待模型服务08return "模型回复"0910@app.get("/chat")11async def chat():12result = await call_model()13return {"result": result}
当 call_model() 等待模型响应时,同一个 worker 可以继续处理其他请求。这里不会因为 100 个请求就默认创建 100 个线程,多个请求通常表现为由事件循环调度的多个协程。
不过,只有底层调用本身支持异步,这种写法才有意义。如果在 async def 接口中调用同步 HTTP 客户端或执行耗时计算,它仍然会阻塞事件循环。
还要避免在 FastAPI 接口中调用 asyncio.run()。服务器已经有正在运行的事件循环,接口只需要使用 await。LangChain 的 ainvoke()、astream() 等异步方法也可以按照这种方式接入。