主题
如果你是异步编程的新手,这一页就是为你准备的。
具体到 LlamaIndex,很多操作和函数都支持异步执行。这让你可以同时运行多个操作而不阻塞主线程,在很多场景下能显著提升整体吞吐与性能。
以下是你需要理解的几个关键概念:
1. asyncio 基础
事件循环(Event Loop):事件循环负责异步操作的调度与执行。它不断检查并执行任务(协程)。所有异步操作都由这个循环来跑,而每个线程只能有一个事件循环。
asyncio.run():这个函数是运行异步程序的入口。它创建并管理事件循环,并在结束后做好清理。请记住,它的设计是每个线程只调用一次。像 FastAPI 这样的框架会替你运行事件循环,另一些场景则需要你自己来运行。异步 + Python Notebook:Python notebook 是一种特殊情况——事件循环已经在运行了。也就是说你不必自己调用
asyncio.run(),可以直接调用并await异步函数。
2. 异步函数与 await
定义异步函数:用
async def语法定义一个异步函数(协程)。调用异步函数不会立即执行,而是返回一个需要被调度运行的协程对象。使用
await:在异步函数内部,await会暂停该函数的执行,直到被等待的任务完成。当你写await some_fn()时,函数会把控制权交还事件循环,让其他任务得以被调度和运行。同一时刻只有一个异步函数在执行,它们通过await让出控制权来协作。
3. 并发是怎么发生的
协作式并发:虽然你可以调度多个异步任务,但同一时刻只有一个任务在执行。这与真正的并行不同——并行是多个任务同时运行。当任务遇到
await时,它会挂起自己,让另一个任务得以运行。这使得异步程序非常适合有大量等待的 I/O 密集型任务,例如调用 LLM 和其他服务的 API。并非真正并行:asyncio 提供的是并发,而不是并行地运行任务。对于需要真正并行的 CPU 密集型工作,请考虑多线程或多进程。LlamaIndex 在多数情况下避免使用多进程,而把选择权交给用户,因为要想做得安全又高效并不容易。
4. 处理阻塞(同步)代码
asyncio.to_thread():有时你需要在不冻结异步程序的前提下运行同步(阻塞)代码。asyncio.to_thread()会把阻塞代码交给一个单独的线程,从而让事件循环继续处理其他任务。请谨慎使用,它会带来一些额外开销,也会让调试变得更困难。另一种做法:执行器(Executors):你也许还会见到用
loop.run_in_executor()来处理阻塞函数。
5. 一个实际例子
下面这个例子演示如何用 asyncio 编写并运行异步函数:
import asyncio
async def fetch_data(delay):
print(f"Started fetching data with {delay}s delay")
# Simulates I/O-bound work, such as network operation
await asyncio.sleep(delay)
print("Finished fetching data")
return f"Data after {delay}s"
async def main():
print("Starting main")
# Schedule two tasks concurrently
task1 = asyncio.create_task(fetch_data(2))
task2 = asyncio.create_task(fetch_data(3))
# Wait until both tasks complete
result1, result2 = await asyncio.gather(task1, task2)
print(result1)
print(result2)
print("Main complete")
if __name__ == "__main__":
asyncio.run(main())