在BaseProxy上调用方法时如何有效使用asyncio?

时间:2019-02-11 18:15:30

标签: python python-multiprocessing python-asyncio

我正在使用一个使用LevelDB并且使用多个长期存在的进程执行不同任务的应用程序。

由于LevelDB只允许一个进程维护数据库连接,因此我们所有的数据库访问都通过特殊的数据库进程进行。

要从另一个进程访问数据库,我们使用BaseProxy。但是,由于我们使用的是asyncio,因此我们的代理不应阻塞这些调用db进程的API,这些API最终会从db中读取。因此,我们使用执行程序在代理上实现API。

    loop = asyncio.get_event_loop()

    return await loop.run_in_executor(
        thread_pool_executor,
        self._callmethod,
        method_name,
        args,
    )

虽然效果很好,但我想知道是否有更好的替代方法,可以将_callmethod的{​​{1}}调用包装在BaseProxy中。

据我了解,ThreadPoolExecutor调用数据库进程是等待IO的教科书示例,因此为此使用线程似乎是不必要的浪费。

在理想情况下,我假设BaseProxy上存在一个async _acallmethod,但不幸的是该API不存在。

因此,我的问题基本上可以归结为:使用BaseProxy时,有没有比BaseProxy中运行这些跨进程调用更有效的选择了?

3 个答案:

答案 0 :(得分:5)

不幸的是,多处理库不适合转换为asyncio,如果必须使用BaseProxy处理IPC(进程间通信),那么您所能做的就是最好的。

虽然该库确实使用了阻塞I / O,但是您在这里不容易进入并重新处理阻塞部分以使用非阻塞原语。如果您坚持采用这种方法,则必须修补或重写该库的内部实现细节,但是作为内部实现细节,这些内容可能会因Python点发行版本而有所不同,从而使任何修补程序均易碎且容易中断Python升级。 _callmethod方法是涉及线程,套接字或管道连接以及序列化程序的深层抽象层次的一部分。参见multiprocessing/connection.pymultiprocessing/managers.py

因此,您的选择是继续使用当前方法(使用线程池执行程序将BaseProxy._callmethod()推送到另一个线程) o r,以使用异步原语来实现自己的IPC解决方案。中央数据库访问进程将充当服务器,供其他进程使用套接字或命名管道,并使用针对客户端请求和服务器响应的商定序列化方案,以客户端身份连接到客户端。这是multiprocessing为您实现的,但是您将使用asyncio streams和最适合您的应用程序模式的任何序列化方案(例如,pickle,JSON,protobuffers或其他)来实现自己的(简单)版本完全)。

答案 1 :(得分:1)

您需要一个线程池。 aioprocessing提供了一些多处理的异步功能,但是它按照您的建议使用线程来完成。如果没有人公开真正的异步多处理,我建议对python提出一个问题。

https://github.com/dano/aioprocessing

  

在大多数情况下,该库通过在ThreadPoolExecutor中执行调用来使对多处理方法的阻塞调用异步进行

答案 2 :(得分:0)

假设您的python和数据库在同一系统中运行(即,您不想async进行任何网络调用),则有两种选择。

  1. 您已经在做什么(在执行程序中运行)。它阻止了db线程,但是主线程仍然可以做其他事情。这不是纯粹的非阻塞,但对于I / O阻塞情况而言,这是可以接受的解决方案,并且维护线程的开销很小。

  2. 对于真正的非阻塞解决方案(可以在单个线程中运行而不会阻塞),您必须拥有#1。对于每个提取调用,数据库对async(回调)的本机支持以及#2将其包装在您的自定义事件循环实现中。在这里,您可以对Base循环进行子类化,并覆盖方法以集成数据库回调。例如,您可以创建一个实现管道服务器的基本循环。 db写入管道,而python轮询管道。请参见asyncio代码库中的Proactor事件循环的实现。注意:我从未实现过任何自定义事件循环。

我对leveldb并不熟悉,但是对于键值存储,尚不清楚这种用于fetch和纯非阻塞实现的回调是否会带来任何明显的好处。如果您在一个迭代器中遇到多个访存问题,而这是您的主要问题,则可以进行循环async(每个访存仍处于阻塞状态)并提高性能。下面是解释此问题的虚拟代码。

import asyncio
import random
import time

async def talk_to_db(d):
    """ 
        blocking db iteration. sleep is the fetch function.
    """
    for k, v in d.items():
        time.sleep(1)
        yield (f"{k}:{v}")

async def talk_to_db_async(d):
    """ 
        real non-blocking db iteration. fetch (sleep) is native async here 
    """
    for k, v in d.items():
        await asyncio.sleep(1)
        yield (f"{k}:{v}")

async def talk_to_db_async_loop(d):
    """ 
        semi-non-blocking db iteration. fetch is blocking, but the
        loop is not.
    """
    for k, v in d.items():
        time.sleep(1)
        yield (f"{k}:{v}")
        await asyncio.sleep(0)

async def db_call_wrapper(db):
    async for row in talk_to_db(db):
        print(row)

async def db_call_wrapper_async(db):
    async for row in talk_to_db_async(db):
        print(row)

async def db_call_wrapper_async_loop(db):
    async for row in talk_to_db_async_loop(db):
        print(row)

async def func(i):
    await asyncio.sleep(5)
    print(f"done with {i}")

database = {i:random.randint(1,20) for i in range(20)}

async def main():
    db_coro = db_call_wrapper(database)
    coros = [func(i) for i in range(20)]
    coros.append(db_coro)
    await asyncio.gather(*coros)

async def main_async():
    db_coro = db_call_wrapper_async(database)
    coros = [func(i) for i in range(20)]
    coros.append(db_coro)
    await asyncio.gather(*coros)

async def main_async_loop():
    db_coro = db_call_wrapper_async_loop(database)
    coros = [func(i) for i in range(20)]
    coros.append(db_coro)
    await asyncio.gather(*coros)

# run the blocking db iteration
loop = asyncio.get_event_loop()
loop.run_until_complete(main())

# run the non-blocking db iteration
loop = asyncio.get_event_loop()
loop.run_until_complete(main_async())

# run the non-blocking (loop only) db iteration
loop = asyncio.get_event_loop()
loop.run_until_complete(main_async_loop())

这是您可以尝试的方法。否则,我会说您当前的方法非常有效。我认为BaseProxy无法为您提供异步acall API,它不知道如何处理数据库中的回调。