python — как реализовать C-функцию как ожидаемую (сопрограмму)

Среда: совместная RTOS на C и виртуальная машина micropython — одна из задач.

Чтобы виртуальная машина не блокировала другие задачи RTOS, я вставляю RTOS_sleep() в vm.c:DISPATCH(), так что после выполнения каждого байт-кода виртуальная машина передает управление следующей задаче RTOS.

Я создал интерфейс uPy для асинхронного получения данных из физической шины данных — это может быть CAN, SPI, Ethernet — с использованием шаблона проектирования производитель-потребитель.

Использование в uPy:

can_q = CANbus.queue()
message = can_q.get()

Реализация на C такова, что can_q.get() НЕ блокирует RTOS: он опрашивает C-очередь и, если сообщение не получено, вызывает RTOS_sleep(), чтобы дать другой задаче возможность заполнить очередь. Вещи синхронизируются, потому что C-очередь обновляется только другой задачей RTOS, а задачи RTOS переключаются только тогда, когда вызывается RTOS_sleep(), т. е. совместная

C-реализация в основном:

// gives chance for c-queue to be filled by other RTOS task
while(c_queue_empty() == true) RTOS_sleep(); 
return c_queue_get_message();

Хотя оператор Python can_q.get() не блокирует RTOS, он блокирует сценарий uPy. Я хотел бы переписать его, чтобы я мог использовать его с async def, то есть с сопрограммой, и чтобы он не блокировал скрипт uPy.

Не уверен в синтаксисе, но что-то вроде этого:

can_q = CANbus.queue()
message = await can_q.get()

ВОПРОС

Как мне написать C-функцию, чтобы я мог await работать с ней?

Я бы предпочел ответ CPython и micropython, но я бы принял ответ только для CPython.


person Bob    schedule 25.06.2018    source источник


Ответы (1)


Примечание: этот ответ охватывает CPython и инфраструктуру asyncio. Однако концепции должны применяться к другим реализациям Python, а также к другим асинхронным платформам.

Как мне написать C-функцию, чтобы я мог await работать с ней?

Самый простой способ написать C-функцию, результат которой можно ожидать, — вернуть уже созданный ожидаемый объект, например asyncio.Future. Прежде чем возвращать Future, код должен обеспечить установку будущего результата с помощью какого-либо асинхронного механизма. Все эти подходы, основанные на сопрограммах, предполагают, что ваша программа работает в некотором цикле обработки событий, который знает, как планировать сопрограммы.

Но возврата будущего не всегда достаточно — может быть, мы хотели бы определить объект с произвольным количеством точек подвеса. Возврат фьючерса приостанавливается только один раз (если возвращенное фьючерс не завершено), возобновляется после завершения фьючерса, и все. Ожидаемый объект, эквивалентный async def, который содержит более одного await, не может быть реализован путем возврата будущего, он должен реализовать протокол, который обычно реализуют сопрограммы. Это похоже на итератор, реализующий пользовательский __next__, и его можно использовать вместо генератора.

Определение пользовательского ожидания

Чтобы определить наш собственный ожидаемый тип, мы можем обратиться к PEP 492, который определяет какие именно объекты можно передать await. В отличие от функций Python, определенных с помощью async def, пользовательские типы могут делать объекты ожидаемыми, определяя специальный метод __await__, который Python/C сопоставляется с частью tp_as_async.am_await структуры PyTypeObject.

Это означает, что в Python/C вы должны сделать следующее:

Чтобы сопрограмма была полезной, она также должна иметь возможность взаимодействовать с управляющим ею циклом событий, чтобы она могла указать, когда она должна быть возобновлена ​​после ее приостановки. Большинство сопрограмм, определенных с помощью asyncio, ожидают, что они будут выполняться в цикле событий asyncio, и внутренне используют asyncio.get_event_loop() (и/или принимают явный аргумент loop) для получения своих служб.

Пример сопрограммы

Чтобы проиллюстрировать, что должен реализовать код Python/C, давайте рассмотрим простую сопрограмму, выраженную как Python async def, например, этот эквивалент asyncio.sleep():

async def my_sleep(n):
    loop = asyncio.get_event_loop()
    future = loop.create_future()
    loop.call_later(n, future.set_result, None)
    await future
    # we get back here after the timeout has elapsed, and
    # immediately return

my_sleep создает Future и организует его выполнение ( его результат становится установленным) через n секунд и приостанавливается до тех пор, пока не завершится будущее. В последней части используется await, где await x означает «позволить x решить, будем ли мы сейчас приостанавливать или продолжать выполнение». Незавершенное будущее всегда принимает решение о приостановке, а специальные случаи драйвера сопрограммы asyncio Task дают будущее, чтобы приостановить их на неопределенный срок и связывают их завершение с возобновлением задачи. Механизмы приостановки других циклов событий (curio и т. д.) могут различаться в деталях, но основная идея та же самая: await — необязательная приостановка выполнения.

__await__(), который возвращает генератор

Чтобы перевести это на C, мы должны избавиться от волшебного определения функции async def, а также от точки приостановки await. Удалить async def довольно просто: эквивалентная обычная функция просто должна вернуть объект, реализующий __await__:

def my_sleep(n):
    return _MySleep(n)

class _MySleep:
    def __init__(self, n):
        self.n = n

    def __await__(self):
        return _MySleepIter(self.n)

Метод __await__ объекта _MySleep, возвращенного my_sleep(), будет автоматически вызываться оператором await для преобразования ожидаемого объекта (всего, что передается await) в итератор. Этот итератор будет использоваться для запроса ожидаемого объекта, выбирает ли он приостановку или предоставление значения. Это очень похоже на то, как оператор for o in x вызывает x.__iter__() для преобразования итерируемого x в конкретный итератор.

Когда возвращенный итератор выбирает приостановку, ему просто нужно произвести значение. Значение значения, если таковое имеется, будет интерпретироваться драйвером сопрограммы, обычно являющимся частью цикла обработки событий. Когда итератор решает прекратить выполнение и вернуться из await, ему необходимо прекратить итерацию. Используя генератор в качестве удобной реализации итератора, _MySleepIter будет выглядеть так:

def _MySleepIter(n):
    loop = asyncio.get_event_loop()
    future = loop.create_future()
    loop.call_later(n, future.set_result, None)
    # yield from future.__await__()
    for x in future.__await__():
        yield x

Поскольку await x сопоставляется с yield from x.__await__(), наш генератор должен исчерпать итератор, возвращенный future.__await__(). Итератор, возвращаемый Future.__await__, даст результат, если будущее не завершено, и вернет результат будущего (который мы здесь игнорируем, но yield from фактически обеспечивает) в противном случае.

__await__(), который возвращает пользовательский итератор

Последним препятствием для реализации my_sleep на языке C является использование генератора для _MySleepIter. К счастью, любой генератор можно преобразовать в итератор с состоянием, __next__ которого выполняет часть кода до следующего ожидания или возврата. __next__ реализует версию кода генератора в виде конечного автомата, где yield выражается возвратом значения, а return - повышением StopIteration. Например:

class _MySleepIter:
    def __init__(self, n):
        self.n = n
        self.state = 0

    def __iter__(self):  # an iterator has to define __iter__
        return self

    def __next__(self):
        if self.state == 0:
            loop = asyncio.get_event_loop()
            self.future = loop.create_future()
            loop.call_later(self.n, self.future.set_result, None)
            self.state = 1
        if self.state == 1:
            if not self.future.done():
                return next(iter(self.future))
            self.state = 2
        if self.state == 2:
            raise StopIteration
        raise AssertionError("invalid state")

Перевод на С

Вышеупомянутый пример довольно типичен, но он работает и использует только те конструкции, которые могут быть определены с помощью собственных функций Python/C.

На самом деле перевод двух классов на C довольно прост, но выходит за рамки этого ответа.

person user4815162342    schedule 30.06.2018