【问题标题】:Python run_in_executor and forget?Python run_in_executor 忘了?
【发布时间】:2019-02-18 14:17:06
【问题描述】:

我怎样才能设置一个阻塞函数在执行器中运行,结果并不重要,所以主线程不应该等待或被它拖慢。

说实话,我不确定这是否是正确的解决方案,我想要的只是将某种类型的处理队列与主进程分开,这样它就不会阻止服务器应用程序返回请求,因为这种类型的 Web 服务器为多个请求运行一个工作器。

我最好远离像 Celery 这样的解决方案,但如果这是最理想的,我愿意学习它。

这里的上下文是一个异步网络服务器,它生成带有大图像的 pdf 文件。

app = Sanic()
#App "global" worker
executor = ProcessPoolExecutor(max_workers=5)

app.route('/')
async def getPdf(request):
  asyncio.create_task(renderPdfsInExecutor(request.json))
  #This should be returned "instantly" regardless of pdf generation time
  return response.text('Pdf being generated, it will be sent to your email when done')

async def renderPdfsInExecutor(json):
  asyncio.get_running_loop.run_in_executor(executor, syncRenderPdfs, json)

def syncRenderPdfs(json)
  #Some PDF Library that downloads images synchronously
  pdfs = somePdfLibrary.generatePdfsFromJson(json)
  sendToDefaultMail(pdfs)

上面的代码给出了错误(是的,它以管理员身份运行):

PermissionError [WinError 5] Access denied
Future exception was never retrieved

额外的问题:通过在执行程序中运行异步循环,我能获得什么吗?因此,如果它一次处理多个 PDF 请求,它将在它们之间分配处理。如果是,我该怎么做?

【问题讨论】:

  • 如果我等待run_in_executor,它不会阻塞主循环,但我会阻止函数/响应“立即”返回
  • @freakish 它不会阻塞循环,但会阻塞 getPdf 函数,因为它正在等待,至少当我直接测试时是这样的

标签: python python-3.x python-asyncio python-3.7 sanic


【解决方案1】:

好的,所以首先有一个误解。这个

async def getPdf(request):
    asyncio.create_task(renderPdfsInExecutor(request.json))
    ...

async def renderPdfsInExecutor(json):
    asyncio.get_running_loop.run_in_executor(executor, syncRenderPdfs, json)

是多余的。做就够了

async def getPdf(request):
    asyncio.get_running_loop.run_in_executor(executor, syncRenderPdfs, request.json)
    ...

或者(因为你不想等待)更好

async def getPdf(request):
    executor.submit(syncRenderPdfs, request.json)
    ...

现在你遇到的问题是因为syncRenderPdfs 抛出了PermissionError。它没有被处理,所以 Python 会警告你“嘿,一些后台代码抛出了一个错误。但是代码不属于任何人,所以这到底是怎么回事?”。这就是你得到Future exception was never retrieved 的原因。 您的 pdf 库本身有问题,而不是 asyncio。一旦你解决了这个内部问题,确保安全也是一个好主意:

def syncRenderPdfs(json)
    try:
        #Some PDF Library that downloads images synchronously
        pdfs = somePdfLibrary.generatePdfsFromJson(json)
        sendToDefaultMail(pdfs)
    except Exception:
        logger.exception('Something went wrong')  # or whatever

您的“权限被拒绝”问题完全不同,您应该对其进行调试和/或为此发布一个单独的问题。

至于最后一个问题:是的,executor 会排队并在worker之间平均分配任务。

编辑:正如我们在 cmets 中谈到的那样,实际问题可能出在您使用的 Windows 环境中。或者更准确地说,使用 ProcessPoolExecutor,即生成进程可能会更改权限。我建议使用 ThreadPoolExecutor,假设它在平台上运行良好。

【讨论】:

  • 但是如果我在执行程序之外运行它,权限被拒绝就不会发生
  • 同样按照你的理论,你不需要awaitrun_in_executor吗?
  • @Mojimi 不,你的重点是在后台运行任务,对吧?无论如何,权限被拒绝问题是另一回事。这可能是因为您使用了生成进程的ProcessPoolExecutor。试试ThreadPoolExecutor
  • 那么协程即使没有调用 await 也会被执行吗?没有意识到这一点。
  • @Mojimi 不,协程/任务在传递给.create_task 或通过.run_in_executor 创建时执行(或在其他一些罕见的情况下)。在那种情况下,他们不需要等待。但并非在所有情况下。至于您的权限被拒绝:我不知道为什么 Windows 不能与 ThreadPoolExecutor 一起使用。无论如何,您的环境似乎有问题。
【解决方案2】:

您可以查看 asyncio.gather(*tasks) 以并行运行多个。

请记住,并行任务只有在 io 绑定且不阻塞时才能正常工作。

python 文档中的一个示例 (https://docs.python.org/3/library/asyncio-task.html):

import asyncio

async def factorial(name, number):
    f = 1
    for i in range(2, number + 1):
        print(f"Task {name}: Compute factorial({number}), currently i={i}...")
        await asyncio.sleep(1)
        f *= i
    print(f"Task {name}: factorial({number}) = {f}")
    return f

async def main():
    # Schedule three calls *concurrently*:
    L = await asyncio.gather(
        factorial("A", 2),
        factorial("B", 3),
        factorial("C", 4),
    )
    print(L)

asyncio.run(main())

【讨论】:

  • 如果您能更好地解释这一点,这将非常有帮助,您可以添加代码示例来说明如何定义“任务”列表并将它们发送到“收集”函数。
猜你喜欢
  • 2021-12-14
  • 2020-08-05
  • 1970-01-01
  • 1970-01-01
  • 2013-09-23
  • 2012-04-21
  • 2011-09-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多