Python 如何在异步函数中并行化for循环并跟踪for循环的执行状态?

Python 如何在异步函数中并行化for循环并跟踪for循环的执行状态?,python,python-3.x,python-asyncio,fastapi,joblib,Python,Python 3.x,Python Asyncio,Fastapi,Joblib,最近,我问了一个问题,关于如何在部署的API中跟踪for循环的进度。这是我的建议 对我有效的方法是 from fastapi import FastAPI, UploadFile from typing import List import asyncio import uuid context = {'jobs': {}} app = FastAPI() async def do_work(job_key, files=None): iter_over = files if

最近,我问了一个问题,关于如何在部署的API中跟踪for循环的进度。这是我的建议

对我有效的方法是

from fastapi import FastAPI, UploadFile
from typing import List
import asyncio
import uuid


context = {'jobs': {}}

app = FastAPI()


async def do_work(job_key, files=None):
    iter_over = files if files else range(100)
    for file, file_number in enumerate(iter_over):
        jobs = context['jobs']
        job_info = jobs[job_key]
        job_info['iteration'] = file_number
        job_info['status'] = 'inprogress'
        await asyncio.sleep(1)
    jobs[job_key]['status'] = 'done'


@app.get('/')
async def get_testing():
    identifier = str(uuid.uuid4())
    context['jobs'][identifier] = {}
    asyncio.run_coroutine_threadsafe(do_work(identifier), loop=asyncio.get_running_loop())

    return {"identifier": identifier}


@app.get('/status/{identifier}')
async def status(identifier):
    return {
        "status": context['jobs'].get(identifier, 'job with that identifier is undefined'),
    }
这样,我可以通过调用
status方法来使用标识符跟踪
do\u work
中for循环的进度

现在,我正在寻找一种方法来并行化
do\u work
方法中的for循环

但是如果我使用
joblib
,那么我不知道如何跟踪正在处理的每个文件,迭代计数将毫无意义,因为所有文件都将并行处理

注意:我只是举了一个关于joblib的例子,因为我对其他库不太熟悉。对文件的处理是基于cpu的有点繁重的工作。我正在预处理文件,加载4个tensorflow模型,并在文件中预测它,然后写入sql数据库


如果有人知道我可以做这件事的任何方法,请分享并帮助我。

我不能100%确定我是否理解,是否需要一些类似的方法

async def do_work(作业密钥,文件=None):
iter_over=如果文件在其他范围内(100),则为文件
作业=上下文[“作业”]
作业信息=作业[作业密钥]
作业信息['iteration']=0
异步def do_work_inner(文件):
#在这里处理文件
作业信息['iteration']+=1
等待异步睡眠(0.5)
tasks=[do_work_inner(文件)for file in iter_over]
作业信息['status']='inprogress'
等待asyncio.gather(*任务)
作业[作业密钥]['status']=“完成”
这将并行运行文件上的所有工作*,请记住,在这种情况下,job_info['iteration']基本上没有意义,因为它们都是从一起开始的,它们将一起增加值

  • 这是异步并行的,这意味着它不是并行的,但事件循环将不断地从一个任务跳到另一个任务
请注意,这一点非常重要,您希望在文件上执行的实际工作类型是什么,如果是与cpu相关的工作(计算、分析等),而不是主要与IO相关的工作(如web调用),那么这是错误的解决方案,应该稍作调整,如果是,请让我知道,我将尝试更新它

编辑:cpu相关工作的更新版本,进度显示文件已完成

这是一个相对完整的示例,只是没有实际的服务器

导入时间
导入异步
随机输入
从concurrent.futures导入ProcessPoolExecutor
作业={}
上下文={}
executor=ProcessPoolExecutor()
每个文件的def do_work_(文件,文件编号):
#CPU相关的工作在这里,这个方法不是异步的
#在这里处理文件
打印(f'开始处理文件{file_number}')
时间.睡眠(随机.随机数(1,10)/10)
返回文件号
异步def do_work(作业密钥,文件=无):
iter_over=文件,如果文件在其他范围(15)
作业=上下文[“作业”]
作业信息=作业[作业密钥]
作业信息['completed']=0
loop=asyncio.get\u running\u loop()
tasks=[loop.run_in_executor(executor,do_work_per_file,file,file_number)for file,file_number in enumerate(iter_over)]
作业信息['status']='inprogress'
对于异步IO中已完成的\u作业。完成时(任务):
打印(f'文件上的已完成工作{wait completed_job}')
作业信息['completed']+=1
打印('当前作业状态为',作业信息)
作业[作业密钥]['status']=“完成”
打印('当前作业状态为',作业信息)
如果uuuu name uuuuuu='\uuuuuuu main\uuuuuuu':
上下文['jobs']=jobs
作业['abc']={}
asyncio.run(do_work('abc'))
输出是

Starting work on file 0
Starting work on file 1
Starting work on file 2
Starting work on file 3
Starting work on file 4
Starting work on file 5
Starting work on file 6
Starting work on file 7
Starting work on file 8
Starting work on file 9
Starting work on file 10
Starting work on file 11
Starting work on file 12
Starting work on file 13
Starting work on file 14
Finished work on file 1
Current job status is  {'completed': 1, 'status': 'inprogress'}
Finished work on file 7
Current job status is  {'completed': 2, 'status': 'inprogress'}
Finished work on file 9
Current job status is  {'completed': 3, 'status': 'inprogress'}
Finished work on file 12
Current job status is  {'completed': 4, 'status': 'inprogress'}
Finished work on file 11
Current job status is  {'completed': 5, 'status': 'inprogress'}
Finished work on file 13
Current job status is  {'completed': 6, 'status': 'inprogress'}
Finished work on file 4
Current job status is  {'completed': 7, 'status': 'inprogress'}
Finished work on file 14
Current job status is  {'completed': 8, 'status': 'inprogress'}
Finished work on file 0
Current job status is  {'completed': 9, 'status': 'inprogress'}
Finished work on file 6
Current job status is  {'completed': 10, 'status': 'inprogress'}
Finished work on file 2
Current job status is  {'completed': 11, 'status': 'inprogress'}
Finished work on file 3
Current job status is  {'completed': 12, 'status': 'inprogress'}
Finished work on file 8
Current job status is  {'completed': 13, 'status': 'inprogress'}
Finished work on file 5
Current job status is  {'completed': 14, 'status': 'inprogress'}
Finished work on file 10
Current job status is  {'completed': 15, 'status': 'inprogress'}
Current job status is  {'completed': 15, 'status': 'done'}

基本上改变的是,现在您正在打开一个处理文件工作的新进程池,成为一个新进程也意味着CPU密集型工作将不会阻止事件循环并阻止您查询作业状态。

EDIT 似乎
joblib
Parallel
函数正在阻塞响应请求的线程。 一个可能更好的解决方案是@Ron Serruya,他们设法不阻塞主线程

旧答案 这里有一个潜在的解决方案。请注意,我没有测试,但它应该足以给你一个粗略的想法。另外,我不是100%确定你需要什么,所以在运行它之前,它肯定需要你方的审查

尽管如此,我不明白为什么不使用数据库来保持迭代的状态。这样,您就避免了并发(或rails)问题,并且即使在断电的情况下也可以保持迭代/训练的状态

from fastapi import FastAPI, UploadFile
from typing import List
import asyncio
import uuid
from joblib import Parallel, delayed


context = {'jobs': {}}

app = FastAPI()

def parallelize(iterate_count):
    # Function part that needs to be run in parallel
    iterate_count += 1
    

async def do_work(job_key, files=None):
    iter_over = files if files else range(100)
    jobs = context['jobs'][job_key]
    jobs["iteration"] = 0
    jobs['status'] = 'inprogress'
    Parallel()(delayed(parallelize)(jobs["iteration"]) for file, file_number in enumerate(iter_over))
    jobs['status'] = 'done'


@app.get('/')
async def get_testing():
    identifier = str(uuid.uuid4())
    context['jobs'][identifier] = {}
    asyncio.run_coroutine_threadsafe(do_work(identifier), loop=asyncio.get_running_loop())

    return {"identifier": identifier}


@app.get('/status/{identifier}')
async def status(identifier):
    return {
        "status": context['jobs'].get(identifier, 'job with that identifier is undefined'),
    }

如果您接收到许多请求,并且处理时间很长,那么跨多个线程并行化工作可能会导致API客户机资源匮乏。因此,请确保将每次调用的线程数(或进程/执行器-见下文)限制在一个较小的数目

您可以使用将文件路径分发给执行器(每个执行器作为一个进程运行),执行器将完成这项工作,您可以在每台机器上有多个执行器,并且可以跨多台机器分发

另一种选择是通过使用线程池,使用
max\u workers
限制每个请求的线程数


并在启动时将一个并发集合传递给线程的,这样线程就可以通过向该集合写入来“报告”其进度(您可以用锁包装一个常规集合,因为Python不提供基于自旋锁的并发集合).

是否有理由需要使用joblib而不是
asyncio运行它。运行\u coroutine\u threadsafe
?有了这样的函数,你可以共享变量,这可能是一个伟大而简单的想法,在我看来,我只是举了一个
joblib
的例子,因为我熟悉这个库,我所要做的就是使for循环执行并行,而不丢失跟踪正在进行的迭代的功能,因此我也会跟踪进度。根据
joblib
的文档,it loo