文章目录
前言
看langchain-chatchat代码的时候看到这一段,本来打算一眼过,忽然发现这个名字上看起来是线程池的地方居然可以用 await。可以把协程的编程方法用在这里,很神奇。
看了下引入的包 from fastapi.concurrency import run_in_threadpool
,追踪进去 实际上是这样
from starlette.concurrency import run_in_threadpool as run_in_threadpool # noqa
那么我们就学习一下这个 run_in_threadpool
的用法
一、run_in_threadpool是什么?
在FastAPI 库中引入了 Starlette 的一些功能和组件,其中包括用于在异步环境中运行同步代码的 run_in_threadpool
函数。Starlette 是一个轻量级的 ASGI 框架,提供了构建高性能异步 Web 应用程序的核心组件,而 FastAPI 则是在 Starlette 的基础上构建的,提供了更高级的功能和简化的接口。
二. run_in_threadpool
的工作原理
在 Starlette(以及被 FastAPI 使用的版本)中,run_in_threadpool
函数是通过将任务提交给 concurrent.futures.ThreadPoolExecutor
实例来实现的。这种方法允许我们在异步代码中运行阻塞的同步代码。
让我们逐步解析 run_in_threadpool
的实现及其工作原理。
1. 创建或获取事件循环
首先,需要获取当前的事件循环,通常在协程中可以通过 asyncio.get_event_loop()
获取:
import asyncio
loop = asyncio.get_event_loop()
2. 创建线程池
线程池由 concurrent.futures.ThreadPoolExecutor
提供。在实际实现中,线程池是一个全局单例,这样可以避免频繁创建和销毁线程池,提高性能。
from concurrent.futures import ThreadPoolExecutor
# 创建全局的线程池实例
executor = ThreadPoolExecutor()
3. 提交任务到线程池并等待结果
使用事件循环的 run_in_executor
方法,将任务提交到线程池执行。run_in_executor
会返回一个 Future
对象,表示异步执行的结果。
async def run_in_threadpool(func, *args, **kwargs):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(executor, func, *args, **kwargs)
4. 完整示例
综合以上步骤,我们来看一个完整的 run_in_threadpool
实现:
import asyncio
from concurrent.futures import ThreadPoolExecutor
# 创建全局的线程池实例
executor = ThreadPoolExecutor()
async def run_in_threadpool(func, *args, **kwargs):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(executor, func, *args, **kwargs)
三. 如何在 FastAPI 中使用
我们来看一个 FastAPI 应用示例,展示如何使用 run_in_threadpool
运行阻塞的同步代码:
from fastapi import FastAPI
from fastapi.concurrency import run_in_threadpool
import time
app = FastAPI()
# 定义一个阻塞的同步函数
def blocking_task():
time.sleep(5)
return "Task Complete"
# 定义一个异步路由处理函数
@app.get("/run-blocking/")
async def run_blocking():
result = await run_in_threadpool(blocking_task)
return {"result": result}
四、使用它的好处
run_in_threadpool
实际上融合了 Python 的多线程和协程两种并发模型,使得它们可以在同一个应用程序中协同工作。这种融合允许你在异步编程环境中执行同步代码,而不会阻塞事件循环。
run_in_threadpool
和 Python 自带的多线程(如 threading
模块)相比,具有一些显著的优势,特别是在异步编程的上下文中。以下是两者之间的一些主要区别和 run_in_threadpool
的优势:
1. 简化异步编程
run_in_threadpool 的主要优势在于它与异步编程模型的无缝集成。使用 run_in_threadpool
,可以在异步函数中直接调用同步函数,并使用 await
等待其完成。这使得代码更加简洁和易读。
from fastapi.concurrency import run_in_threadpool
async def some_async_function():
result = await run_in_threadpool(some_sync_function)
# 继续处理 result
相比之下,使用 threading
需要在异步环境中管理线程的生命周期,并且不能直接使用 await
等待结果,可能需要结合 concurrent.futures.Future
或者其他回调机制,代码会更加复杂。
2. 内置的线程池管理
run_in_threadpool 利用 FastAPI 内置的线程池管理。这意味着你不需要手动创建和管理线程池,减少了管理线程池的复杂性和潜在的资源泄露风险。
import threading
def some_function():
pass
# 使用 threading 手动管理线程
thread = threading.Thread(target=some_function)
thread.start()
thread.join()
在这种情况下,你需要手动管理线程的创建和销毁。而使用 run_in_threadpool
,这些都被框架自动处理了。
3. 更好的与异步框架集成
在使用像 FastAPI 这样的异步框架时,run_in_threadpool 提供了更好的集成。这意味着在处理 HTTP 请求时,可以更高效地调度阻塞的同步任务而不会阻塞事件循环,从而提高并发性能。
from fastapi import FastAPI
from fastapi.concurrency import run_in_threadpool
app = FastAPI()
def blocking_task():
# 一些阻塞操作
pass
@app.get("/")
async def read_root():
result = await run_in_threadpool(blocking_task)
return {"result": result}
如果使用 threading
,代码将会变得更加复杂,不容易维护。
4. 自动处理异常
run_in_threadpool 可以更方便地捕获和处理在线程池中执行的函数抛出的异常,而不需要额外的处理逻辑。这使得代码更加健壮。
from fastapi import FastAPI, HTTPException
from fastapi.concurrency import run_in_threadpool
app = FastAPI()
def blocking_task():
raise Exception("Some error")
@app.get("/")
async def read_root():
try:
result = await run_in_threadpool(blocking_task)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {"result": result}
如果使用 threading
,你需要自己实现异常捕获和处理机制。
总结
run_in_threadpool
实现了多线程和协程的融合,通过将阻塞的同步任务委托给线程池执行,并使用 await
等待任务完成,使得在异步编程环境中能够有效处理同步操作。这样既能利用线程池的并发能力,又能保持事件循环的高效运行,从而提高整体应用程序的性能和响应能力。