【问题标题】:Is there a way to call an `async` python method from C++?有没有办法从 C++ 调用“异步”python 方法?
【发布时间】:2019-06-30 10:08:54
【问题描述】:

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

假设有这段代码:

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

假设代码是从 python 调用的,并且有一个 io 循环处理这个,在某些时候,代码会落入 C++ 领域,需要调用这个 bar 方法 - await 结果如何在 C++ 中?

【问题讨论】:

  • 在删除的答案上重新阅读您的 cmets 后,我很好奇您的呼叫站点(您要放置 await 的位置实际上是什么样的)。是你想用 C++ 实现的async def 吗?
  • @user4815162342 - 这是正确的,在 python 领域 - 有 async def 方法在某些点有 await .. 用于其他异步操作。所以现在 - 我没有 async python 方法,而是有一个 C++ 函数,并且想要在其中实现相同的效果(嗯,类似的东西)

标签: python c++ python-asyncio pybind11


【解决方案1】:

对于这样的事情,如果我不想深入研究 CPython API,我只是用 Python 编写我的东西,并使用pybinds 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));

但是,这很可能不如上述解决方案的性能。

【讨论】:

  • 你不能在非异步函数中使用异步等待。
【解决方案2】:

可以在 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-in-a-loop 听起来像 yield from,那么您是完全正确的,这就是为什么 await 经常被描述为yield from术语。但是,在C++中我们没有yieldyet),所以我们必须将上面的内容集成到状态机中。

要从头实现async def,我们需要一个满足以下约束的类型:

  • 在构造时并没有做太多 - 通常它只会存储它收到的参数
  • 有一个__await__ 方法返回一个可迭代对象,可以是self
  • 有一个__iter__,它返回一个迭代器,它又可以是self
  • 有一个__next__方法,其调用实现了状态机的一步,return表示暂停,raiseStopIteration表示结束。

__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")

我们可以使用真正的 asyncio 测试我们的“协程”是否正常工作:

>>> 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类。

【讨论】:

  • 这是一个很好的问题处理方法,我会试试这个并恢复!谢谢。
  • @Nim 谢谢。这个older answer 中还提供了一些额外的细节,尽管那里的代码过多地依赖于 asyncio,这对于您的用例来说应该是不必要的(尽管如果需要它仍然可以完成)。我认为这个答案更好地抓住了核心思想。
【解决方案3】:

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

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

result = protocol_callPageHandler(self, r->func, request))

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

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

然后你需要使用 add_done_callback 添加一个回调:

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

self->task_done 是一个在python中注册的C函数,当任务完成时会被调用。

【讨论】:

    猜你喜欢
    • 2020-10-10
    • 2016-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-19
    • 1970-01-01
    相关资源
    最近更新 更多