有没有办法从C ++调用`async` python方法?

时间:2019-02-06 12:40:55

标签: python c++ python-asyncio pybind11

我们在python中有一个使用asyncio和协同例程(async方法和await s)的代码库,我想做的是从C ++调用这些方法之一被拉入python的类(使用pybind11)

假设有这样的代码:

class Foo:
  async def bar(a, b, c):
    # some stuff
    return c * a

假设代码是从python调用的,并且有一个处理此问题的io循环,在某些时候,代码会放到需要调用此bar方法的C ++领域-一个{{1 }}在C ++中的结果?

3 个答案:

答案 0 :(得分:3)

这不是pybind11,但是您可以直接从C调用异步函数。您只需使用add_done_callback将回调添加到将来。我假设pybind11允许您调用python函数,因此步骤将是相同的:

https://github.com/MarkReedZ/mrhttp/blob/master/src/mrhttp/internals/protocol.c

PyObject *task;
if(!(task = PyObject_CallFunctionObjArgs(self->create_task, result, NULL))) return NULL;

现在,异步功能的结果是未来。就像在python中一样,您需要使用生成的Future来调用create_task:

add_done_callback = PyObject_GetAttrString(task, "add_done_callback")
PyObject_CallFunctionObjArgs(add_done_callback, self->task_done, NULL)

然后您需要使用add_done_callback添加回调:

const std::string setup_exts[] = { "zst", "xz", "bz2", "ini" };

self-> task_done是在python中注册的C函数,任务完成后将被调用。

答案 1 :(得分:3)

可以在C ++中实现Python协程,但是需要一些工作。您需要执行解释器(使用静态语言的编译器)通常为您执行的操作,然后将异步函数转换为状态机。考虑一个非常简单的协程:

async def coro():
    x = foo()
    y = await bar()
    baz(x, y)
    return 42

调用coro()不会运行任何代码,但是会生成一个 awaitable 对象,该对象可以启动然后多次恢复。 (但是您通常看不到这些操作,因为它们是由事件循环透明地执行的。)可等待对象可以两种不同的方式进行响应:通过1)挂起,或通过2)表示已完成。

在协程await内实现暂停。如果协程程序是由生成器实现的,则y = await bar()将对以下内容进行糖化:

# pseudo-code for y = await bar()

_bar_iter = bar().__await__()
while True:
    try:
        _suspend_val = next(_bar_iter)
    except StopIteration as _stop:
        y = _stop.value
        break
    yield _suspend_val

换句话说,await暂停(屈服),只要等待的对象这样做即可。等待的对象通过提高StopIteration并在其value属性内走私返回值来表示已完成此操作。如果循环收益听起来像yield from,那么您说得很对,这就是为什么await经常用yield from的术语来描述的原因。但是,在C ++中,我们没有yieldyet),因此我们必须将以上内容集成到状态机中。

要从头开始实现async def,我们需要具有满足以下约束的类型:

  • 在构建时不会做很多事情-通常只会存储收到的参数
  • 具有一个__await__方法,该方法返回一个可迭代的对象,该方法可以只是self;
  • 有一个__iter__,它返回一个迭代器,该迭代器也可以是self;
  • 具有一种__next__方法,其调用实现了状态机的一步,其中return表示暂停,而升高StopIteration表示完成。

__next__中的上述协程的状态机将包含三个状态:

  1. 第一个,当它调用foo()同步功能时
  2. 下一个状态,只要它持续等待bar()协程,直到它暂停(传播暂停)给调用者。 bar()返回值后,我们可以立即继续调用baz()并通过StopIteration异常返回值。
  3. 最终状态,仅引发异常,通知呼叫者协程已用完。

因此,以下所示的async def coro()定义可以被视为语法糖:

class coro:
    def __init__(self):
        self._state = 0

    def __iter__(self):
        return self

    def __await__(self):
        return self

    def __next__(self):
        if self._state == 0:
            self._x = foo()
            self._bar_iter = bar().__await__()
            self._state = 1

        if self._state == 1:
            try:
                suspend_val = next(self._bar_iter)
                # propagate the suspended value to the caller
                # don't change _state, we will return here for
                # as long as bar() keeps suspending
                return suspend_val
            except StopIteration as stop:
                # we got our value
                y = stop.value
            # since we got the value, immediately proceed to
            # invoking `baz`
            baz(self._x, y)
            self._state = 2
            # tell the caller that we're done and inform
            # it of the return value
            raise StopIteration(42)

        # the final state only serves to disable accidental
        # resumption of a finished coroutine
        raise RuntimeError("cannot reuse already awaited coroutine")

我们可以使用真正的异步测试我们的“协程”:

>>> class coro:
... (definition from above)
...
>>> def foo():
...     print('foo')
...     return 20
... 
>>> async def bar():
...     print('bar')
...     return 10
... 
>>> def baz(x, y):
...     print(x, y)
... 
>>> asyncio.run(coro())
foo
bar
20 10
42

剩下的部分是用Python / C或pybind11编写coro类。

答案 2 :(得分:0)

对于这样的事情,如果我不想深入研究CPython API,我只是用Python编写我的东西,然后使用pybind的Python接口来调用它。

一个例子: https://github.com/RobotLocomotion/drake/blob/a7700d3/bindings/pydrake/init.py#L44 https://github.com/RobotLocomotion/drake/blob/a7700d3/bindings/pydrake/pydrake_pybind.h#L359

渲染到此用例,也许您可​​以这样做:

# cpp_helpers.py
def await_(obj):
    return await obj
py::object py_await = py::module::import("cpp_helpers").attr("await_");
auto result = py::cast<MyResult>(py_await(py_obj));

但是,这很可能会比上述解决方案性能差。