【算法工程】基于FastAPI实现并发异步任务处理系统

1. 背景介绍 

        近期在FastAPI应用中遇到一个并发问题,采用asyncio的create_task方式,再结合to_thread的方式实现异步化,并发能力很弱,而且当遇到实现方法中存在线程不安全的问题,并发就基本不能使用。因此需要采取其他方案进行解决。

2.  解决思路

       我们将基于 FastAPI 和 ProcessPoolExecutor 来实现异步任务处理系统,用来执行计算密集型的任务。  

  • 进程池并行处理: 使用 concurrent.futures.ProcessPoolExecutor 来启动一个进程池,处理计算密集型任务。之所以在这里采用进程池,是为了确保每个任务运行在独立的 Python 进程中,一方面可以解决遇到的共享参数线程不安全问题,另一方面可以绕过 Python 的 GIL,充分利用多核 CPU。在 Python 中,多个线程共享参数时,可能导致线程不安全的情况,尤其是在并发访问或修改共享数据时。例如,如果多个线程同时修改共享参数,可能会引发数据竞争,导致结果不可预测。

  • 异步接口与后台任务: FastAPI 的异步化通过 async def 定义非阻塞的 HTTP 处理逻辑。接口的核心是 BackgroundTasks,目的是将耗时任务交由后台异步执行,避免阻塞 HTTP 请求响应,当平台发起计算任务后,可以先快速回复信息,避免客户端长时间等待,然后实际的任务在后台进行处理。

  • 任务状态管理: 使用一个全局字典 jobs 存储任务的执行状态和结果,任务通过唯一 ID (UUID) 进行标识,前端可以通过任务 ID 检索状态,并且可以通过该uuid信息,关闭pid进程,杀死不需要的计算任务。

3. 核心功能与实现

3.1 进程池初始化与关闭

        进程池的生命周期与 FastAPI 应用绑定:

  • 在应用启动时,通过 @app.on_event("startup") 创建进程池。

  • 在应用关闭时,通过 @app.on_event("shutdown") 关闭进程池,释放资源。

@app.on_event("startup") 
async def startup_event(): 
    app.state.executor = ProcessPoolExecutor() 
    logger.info("ProcessPoolExecutor initialized.") 

@app.on_event("shutdown") 
async def on_shutdown(): 
    app.state.executor.shutdown() 
    logger.info("ProcessPoolExecutor shutdown.")

3.2 在进程池中异步运行任务

        通过 asyncio.get_event_loop().run_in_executor 实现异步运行:

  • 核心是 run_in_process 方法,它将指定的计算函数compute_something以及参数封装后交由进程池执行。

  • 使用 functools.partial 将函数和参数组合成可序列化的对象,便于传递给进程池。

async def run_in_process(fn, *args, **kwargs): 
    loop = asyncio.get_event_loop() 
    return await loop.run_in_executor(app.state.executor, 
                                      partial(fn, *args, **kwargs))

        实际运行一段时间,发现存在一个问题,超时后不能直接kill任务,因此需要调整为子进程的模式,如下所示:

async def run_in_process(fn, *args, **kwargs):
    """在进程池中异步运行函数,并添加超时控制,确保超时后终止任务"""
    timeout = int(settings.SERVICE["time_out"])
    loop = asyncio.get_event_loop()

    queue = multiprocessing.Queue()
    process = multiprocessing.Process(target=process_wrapper, args=(fn, queue, *args), kwargs=kwargs)
    process.start()

    try:
        return await asyncio.wait_for(loop.run_in_executor(None, queue.get), timeout=timeout)
    except asyncio.TimeoutError:
        logger.error(f"Task execution exceeded timeout of {timeout} seconds. Terminating process...")
        process.terminate()  # 强制终止进程
        process.join()  # 确保进程资源被释放
        raise TimeoutError(f"Task execution exceeded timeout of {timeout} seconds.")
    finally:
        if process.is_alive():
            process.terminate()
            process.join()

3.3 后台任务处理

        后台任务使用 BackgroundTasks,由 FastAPI 提供支持:

  • deep_parse_async 接口通过 BackgroundTasks.add_task 启动任务,并将 start_compute_something_task 作为后台任务的执行函数。

  • 每个任务在后台独立运行,不阻塞 HTTP 响应。

@app.post("/compute_something/async", tags=["compute"], summary="异步接口") 
async def compute_something_async(
        background_tasks: BackgroundTasks,
        example_parameter: str = Form(description="示例参数", 
                                      default="xxx")): 
        uid = uuid4() 
        jobs[uid] = {"status": "in_progress", "result": None}         

        background_tasks.add_task( 
                start_compute_somthing_task, 
                uid, 
                example_parameter = example_parameter)
        return Response(code=200, status="success", message="job created")

3.4 任务执行与状态更新

        后台任务的执行逻辑由 start_compute_something_task 实现:

  • 任务开始时,状态标记为 "in_progress"

  • 调用 run_in_process 将任务提交到进程池执行。

  • 任务完成后,更新状态为 "complete",并保存结果;如果失败,更新状态为 "failed" 并记录错误信息。

async def start_compute_something_task(uid: UUID, **kwargs): 
    try: 
        result = await run_in_process(example_api.real_compute_something, **kwargs)
        jobs[uid]["result"] = result 
        jobs[uid]["status"] = "complete" 
    except Exception as ex: 
        jobs[uid]["status"] = "failed" 
        jobs[uid]["result"] = {"error": str(ex)}

3.5 最大任务数及超时时间功能

        同时也支持配置最大任务数:

1@app.on_event("startup") 
async def startup_event(): 
    app.state.executor = ProcessPoolExecutor(max_workers=MAX_JOB_NUMBER)

        支持超时时间设置:

async def run_in_process(fn, *args, timeout: int = 600, **kwargs): 
    loop = asyncio.get_event_loop() 
    future = loop.run_in_executor(app.state.executor, partial(fn, *args, **kwargs)) 
    try: 
        return await asyncio.wait_for(future, timeout=timeout) 
    except TimeoutError: 
        logger.error(f"Task timed out after {timeout} seconds.") 
        raise Exception(f"Task execution exceeded timeout of {timeout} seconds.")

验证结果 :

        实际测试下来符合预期,比如设置最大workers数量为4,打进来5个请求,首先会处理前4个请求,当其中一个请求完成并释放资源,再执行第5个请求。并且是无阻塞执行,先返回提交状态,再后台执行具体的耗时的计算动作。

4. 参考材料

【1】如何在FastAPI中进行多进程处理-腾讯云开发者社区-腾讯云

### FastAPI 框架中的异步编程指南与示例 FastAPI 是一个现代、快速(高性能)的Web框架,用于构建API,基于Python类型提示。该框架支持异步操作,这使得开发人员可以编写高效的非阻塞代码。 #### 使用 `async` 和 `await` 为了实现异步功能,在定义路径操作函数时应使用关键字 `async def` 来声明它们为协程(coroutine),并在调用其他异步方法时使用 `await` 关键字[^3]: ```python from fastapi import FastAPI app = FastAPI() @app.get("/items/{item_id}") async def read_item(item_id: int): await some_async_function() return {"item_id": item_id} ``` 这里假设存在名为 `some_async_function()` 的异步函数来模拟实际业务逻辑处理过程。 #### 异步数据库交互 当涉及到像读取或写入这样的I/O密集型任务时,推荐采用异步驱动的数据访问库,例如 Tortoise ORM 或 SQLAlchemy 2.0+ 版本。下面是一个简单的例子展示如何利用 Motor 进行 MongoDB 数据库查询: ```python from motor.motor_asyncio import AsyncIOMotorClient from pydantic import BaseModel class Item(BaseModel): name: str description: str | None = None client = AsyncIOMotorClient('mongodb://localhost:27017') db = client.test_database @app.post('/create-item/', response_model=Item) async def create_item(item: Item): result = await db.items.insert_one(item.dict()) created_item = await db.items.find_one({'_id': result.inserted_id}) return created_item ``` 此段代码展示了创建新文档并立即检索刚插入记录的过程。 #### 并发执行多个请求 有时可能希望并发运行几个独立的任务而不必等待每一个完成后再继续下一个;这时就可以借助 Python 内置模块 asyncio 提供的功能——gather() 函数来达到目的: ```python import asyncio async def fetch_data(url): # Simulate network delay with sleep. await asyncio.sleep(1) return {'data': f'Fetched from {url}'} @app.get('/fetch-multiple/') async def fetch_multiple(): urls = ['http://example.com', 'https://another-site.org'] results = await asyncio.gather(*[fetch_data(url) for url in urls]) return results ``` 上述实例中,两个不同的URL被同时抓取数据,并最终一起返回给客户端。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源泉的小广场

感谢大佬的支持和鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值