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

Nim*_*Nim 11 c++ python python-asyncio pybind11

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

假设有这样的代码:

class Foo:
  async def bar(a, b, c):
    # some stuff
    return c * a
Run Code Online (Sandbox Code Playgroud)

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

use*_*342 6

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

async def coro():
    x = foo()
    y = await bar()
    baz(x, y)
    return 42
Run Code Online (Sandbox Code Playgroud)

Invokingcoro()不会运行它的任何代码,但它会生成一个可等待的对象,该对象可以启动然后恢复多次。(但您通常不会看到这些操作,因为它们是由事件循环透明地执行的。)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
Run Code Online (Sandbox Code Playgroud)

换句话说,await只要等待的对象挂起(产生)。等待的对象表示它是通过提高StopIteration和在其value属性中走私返回值来完成的。如果产量在-一环听起来yield from,你是完全正确的,这就是为什么await经常被描述而言yield from。但是,在 C++ 中我们没有yield尚未),因此我们必须将上述内容集成到状态机中。

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

  • 构造时不会做太多事情 - 通常它只会存储它收到的参数
  • 有一个__await__返回可迭代对象的方法,它可以是self
  • 有一个__iter__返回一个迭代器,它可以再次是self;
  • 有一个__next__方法,它的调用实现了状态机的一个步骤,返回意味着暂停,提升StopIteration意味着完成。

上述协程的状态机 in__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")
Run Code Online (Sandbox Code Playgroud)

我们可以使用真正的 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
Run Code Online (Sandbox Code Playgroud)

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


小智 5

这不是 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))
Run Code Online (Sandbox Code Playgroud)

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

PyObject *task;
if(!(task = PyObject_CallFunctionObjArgs(self->create_task, result, NULL))) return NULL;
Run Code Online (Sandbox Code Playgroud)

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

add_done_callback = PyObject_GetAttrString(task, "add_done_callback")
PyObject_CallFunctionObjArgs(add_done_callback, self->task_done, NULL)
Run Code Online (Sandbox Code Playgroud)

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