Асинхронные обратные вызовы и генераторы Python

Я пытаюсь преобразовать синхронную библиотеку для использования внутренней асинхронной структуры ввода-вывода. У меня есть несколько методов, которые выглядят так:

def foo:
  ....
  sync_call_1()   # synchronous blocking call
  ....
  sync_call_2()   # synchronous blocking call
  ....
  return bar

Для каждой из синхронных функций (sync_call_*) я написал соответствующую асинхронную функцию, которая выполняет обратный вызов. Например.

def async_call_1(callback=none):
  # do the I/O
  callback()

Теперь для вопроса о новичке python - что проще всего перевести существующие методы для использования этих новых методов async? То есть метод foo() выше должен быть теперь:

def async_foo(callback):
  # Do the foo() stuff using async_call_*
  callback()

Один очевидный выбор - передать обратный вызов в каждый асинхронный метод, который эффективно "возобновляет" вызывающую функцию "foo", а затем вызывает глобальный вызов в самом конце метода. Однако это делает код хрупким, уродливым, и мне нужно будет добавить новый обратный вызов для каждого вызова метода async_call_*.

Есть ли простой способ сделать это с использованием идиомы python, например, генератора или сопрограммы?

Ответ 1

UPDATE: возьмите это с солью, поскольку я не в курсе современных асинхронных разработок python, включая gevent и asyncio и на самом деле не имеют серьезного опыта работы с асинхронным кодом.


В Python существует 3 общих подхода к асинхронному кодированию без потоков:

  • Обратные вызовы - уродливые, но работоспособные, Twisted делает это хорошо.

  • Генераторы - приятные, но требуют, чтобы весь ваш код соответствовал стилю.

  • Используйте реализацию Python с реальными задачами - Stackless (RIP) и greenlet.

К сожалению, в идеале вся программа должна использовать один стиль, или вещи становятся сложными. Если вы в порядке со своей библиотекой, разоблачающей полностью синхронный интерфейс, вы, вероятно, все в порядке, но если вы хотите, чтобы несколько звонков в вашу библиотеку работали параллельно, особенно параллельно с другим асинхронным кодом, тогда вам нужно общее событие "реактор", может работать со всем кодом.

Итак, если у вас есть (или ожидаете, что у пользователя будет) другой асинхронный код в приложении, то использование этой модели, вероятно, будет разумным.

Если вы не хотите понимать весь беспорядок, подумайте о том, как использовать старые старые темы. Они также уродливы, но работают со всем остальным.

Если вы хотите понять, как сопроводители могут вам помочь, и как они могут вас усложнить, Дэвид Бэзли "Любопытный курс по Corouts и Concurrency" - это хороший материал.

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

Ответ 2

Существует несколько способов для задач мультиплексирования. Мы не можем сказать, что лучше для вашего дела, не имея более глубоких знаний о том, что вы делаете. Вероятно, самым простым/универсальным способом является использование потоков. Взгляните на этот вопрос для некоторых идей.

Ответ 3

Вам нужно сделать функцию foo также асинхронной. Как насчет этого подхода?

@make_async
def foo(somearg, callback):
    # This function is now async. Expect a callback argument.
    ...

    # change 
    #       x = sync_call1(somearg, some_other_arg)
    # to the following:
    x = yield async_call1, somearg, some_other_arg
    ...

    # same transformation again
    y = yield async_call2, x
    ...

    # change
    #     return bar
    # to a callback call
    callback(bar)

И make_async можно определить следующим образом:

def make_async(f):
    """Decorator to convert sync function to async
    using the above mentioned transformations"""
    def g(*a, **kw):
        async_call(f(*a, **kw))
    return g

def async_call(it, value=None):
    # This function is the core of async transformation.

    try: 
        # send the current value to the iterator and
        # expect function to call and args to pass to it
        x = it.send(value)
    except StopIteration:
        return

    func = x[0]
    args = list(x[1:])

    # define callback and append it to args
    # (assuming that callback is always the last argument)

    callback = lambda new_value: async_call(it, new_value)
    args.append(callback)

    func(*args)

ВНИМАНИЕ: я не тестировал этот