Есть ли способ вызвать асинхронный метод Python из С++?

У нас есть кодовая база на python, которая использует asyncio и сопрограммы (методы async и awaits), я хотел бы вызвать один из этих методов из класса C++, который был перенесен в python (используя pybind11)

Допустим, есть этот код:

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

Предполагая, что код вызывается из python и есть цикл ввода-вывода, обрабатывающий это, в какой-то момент код попадает в землю C++, где этот bar метод должен быть вызван - как можно await получить результат этого в C++?


person Nim    schedule 06.02.2019    source источник
comment
После повторного чтения ваших комментариев к удаленному ответу мне любопытно, как выглядит ваш сайт вызова (место, в которое вы хотите поместить await). Это async def, который вы хотите реализовать на C++?   -  person user4815162342    schedule 09.02.2019
comment
@user4815162342 user4815162342 - это правильно, в стране питонов - есть async def методы, которые в точках имеют await .. для других асинхронных операций. Итак, теперь вместо метода async python у меня есть функция C++, и я хочу добиться того же эффекта (ну, что-то похожее)   -  person Nim    schedule 13.02.2019


Ответы (3)


Можно реализовать сопрограмму Python на C++, но это требует некоторой работы. Вам нужно сделать то, что интерпретатор (в статических языках компилятор) обычно делает за вас, и преобразовать вашу асинхронную функцию в конечный автомат. Рассмотрим очень простую сопрограмму:

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

При вызове coro() код не запускается, но создается ожидаемый объект, который можно запускать, а затем возобновлять несколько раз. (Но обычно вы не видите эти операции, потому что они прозрачно выполняются циклом обработки событий.) Ожидаемое может реагировать двумя разными способами: 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++ у нас нет yield (пока), поэтому мы должны интегрировать вышеперечисленное в конечный автомат.

Чтобы реализовать 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")

Мы можем проверить, работает ли наша «сопрограмма», используя настоящий 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

Осталось написать класс coro на Python/C или в pybind11.

person user4815162342    schedule 13.02.2019
comment
Это отличное решение проблемы, я попробую и вернусь! Спасибо. - person Nim; 14.02.2019
comment
@Ним Спасибо. Некоторые дополнительные сведения также присутствуют в этом старом ответе, хотя код слишком сильно зависит от asyncio, что для вашего варианта использования не должно быть необходимым (хотя это все еще может быть сделано, если это необходимо). Я думаю, что этот ответ лучше отражает основную идею. - person user4815162342; 14.02.2019

Это не 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 — это функция C, зарегистрированная в python, которая будет вызываться, когда задача будет выполнена.

person MarkReedZ    schedule 10.02.2019

Для таких вещей, если я не хочу слишком углубляться в 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));

Однако это, скорее всего, будет менее производительным, чем приведенные выше решения.

person eacousineau    schedule 08.03.2019
comment
Вы не можете использовать async await в неасинхронной функции. - person pwuertz; 22.10.2019