如何将同步函数包装在异步协程中?

问题描述 投票:0回答:7

我正在使用 aiohttp 构建一个 API 服务器,将 TCP 请求发送到单独的服务器。发送 TCP 请求的模块是同步的,对于我来说是一个黑匣子。所以我的问题是这些请求阻塞了整个 API。我需要一种方法将模块请求包装在异步协程中,而不会阻塞 API 的其余部分。

那么,仅使用

sleep
作为一个简单的示例,有没有办法以某种方式将耗时的同步代码包装在非阻塞协程中,如下所示:

async def sleep_async(delay):
    # After calling sleep, loop should be released until sleep is done
    yield sleep(delay)
    return 'I slept asynchronously'
python python-3.x asynchronous python-asyncio aiohttp
7个回答
113
投票

最终我在这个帖子中找到了答案。我正在寻找的方法是run_in_executor。这允许同步函数异步运行,而不会阻塞事件循环。

在我上面发布的

sleep
示例中,它可能看起来像这样:

import asyncio
from time import sleep

async def sleep_async(loop, delay):
    # None uses the default executor (ThreadPoolExecutor)
    await loop.run_in_executor(None, sleep, delay)
    return 'I slept asynchronously'

另请参阅以下答案 -> 我们如何在需要协程的地方调用普通函数?


48
投票

您可以使用装饰器将同步版本包装为异步版本。

import time
from functools import wraps, partial


def wrap(func):
    @wraps(func)
    async def run(*args, loop=None, executor=None, **kwargs):
        if loop is None:
            loop = asyncio.get_event_loop()
        pfunc = partial(func, *args, **kwargs)
        return await loop.run_in_executor(executor, pfunc)
    return run

@wrap
def sleep_async(delay):
    time.sleep(delay)
    return 'I slept asynchronously'

已过时,aioify处于维护模式

或使用aioify

% pip install aioify

然后

@aioify
def sleep_async(delay):
    pass

34
投票

从 python 3.9 开始,最简洁的方法是使用 asyncio.to_thread 方法,这基本上是

run_in_executor
的快捷方式,但保留所有上下文变量。

另外,请考虑 GIL,因为它是一个 to_线程。您仍然可以运行 CPU 密集型任务,例如

numpy
。来自文档:

Note Due to the GIL, asyncio.to_thread() can typically only be used to make IO-bound functions non-blocking. However, for extension modules that release the GIL or alternative Python implementations that don’t have one, asyncio.to_thread() can also be used for CPU-bound functions.

同步功能的使用示例:

def blocking_io():
    time.sleep(1)

async def main():
    asyncio.to_thread(blocking_io)


asyncio.run(main())

8
投票

也许有人需要我的解决方案来解决这个问题。我编写了自己的库来解决这个问题,它允许您使用装饰器使任何函数异步。

要安装库,请运行以下命令:

$ pip install awaits

要使任何函数异步,只需向其添加 @awaitable 装饰器,如下所示:

import time
import asyncio
from awaits.awaitable import awaitable

@awaitable
def sum(a, b):
  # heavy load simulation
  time.sleep(10)
  return a + b

现在您可以确保您的函数确实是异步协程:

print(asyncio.run(sum(2, 2)))

“在幕后”,您的函数将在线程池中执行。每次调用函数时都不会重新创建该线程池。线程池创建一次并通过队列接受新任务。这将使您的程序比使用其他解决方案运行得更快,因为创建额外的线程是额外的开销。


8
投票

装饰器对于这种情况很有用,并在另一个线程中运行阻塞函数。

import asyncio
from concurrent.futures import ThreadPoolExecutor
from functools import wraps, partial
from typing import Union

class to_async:

    def __init__(self, *, executor: Optional[ThreadPoolExecutor]=None):
       
        self.executor =  executor
    
    def __call__(self, blocking):
        @wraps(blocking)
        async def wrapper(*args, **kwargs):

            loop = asyncio.get_event_loop()
            if not self.executor:
                self.executor = ThreadPoolExecutor()

            func = partial(blocking, *args, **kwargs)
        
            return await loop.run_in_executor(self.executor,func)

        return wrapper

@to_async(executor=None)
def sync(*args, **kwargs):
    print(args, kwargs)
   
asyncio.run(sync("hello", "world", result=True))


1
投票

不确定是否为时已晚,但您也可以使用装饰器在线程中执行您的功能。不过,请注意,它仍然是非合作阻塞,而不像异步是合作阻塞。

def wrap(func):
    from concurrent.futures import ThreadPoolExecutor
    pool=ThreadPoolExecutor()
    @wraps(func)
    async def run(*args, loop=None, executor=None, **kwargs):
        if loop is None:
            loop = asyncio.get_event_loop()
        future=pool.submit(func, *args, **kwargs)
        return asyncio.wrap_future(future)
    return run

0
投票

Alexey Trofimov 答案的简单旁注:

如果您不关心函数何时执行及其返回(例如保证成功的对数据库的发布请求),您可以如下包装 to_thread 函数

blocking_coro = asyncio.to_thread(blocking)
# execute the blocking function independently
task = asyncio.create_task(blocking_coro)

信用转到这里

© www.soinside.com 2019 - 2024. All rights reserved.