Поздравляю с годом Кролика и желаю сбычи ваших мечт! Хочу вас обрадовать, что, судя по всему, в новом году будет продолжение франшизы про асинхронный django. Версия на гринлетах скоро получит новую, более изящную форму. И новое название - fibers (старое, greenhack, никуда не годилось).

Вот как это выглядит:

async def myview(request):
    async with fibers:
        # Здесь можно использовать django
        obj = MyObject.objects.get()
        print(obj.related_obj)

    # Здесь можно писать асинхронный код
    await notify_somebody(request)

Теперь, использовать django внутри асинхронной функции можно под (асинхронным) контекстным менеджером. По-моему, юзабилити сильно выигрывает. И семантически - тоже понятнее, что происходит, нет?

Пару слов - для тех, кто не читал мои предыдущие статьи. Несмотря на то, что используется django, и код похож на блокирующий - это не так, блокирующих вызовов нет. Всю магию обеспечивают гринлеты. Похожий способ использует sqlalchemy.

Так вот, пример c кодом неплохо смотрится, правда? Вы спросите - почему было сразу так не сделать? Дело в том, что мне только недавно пришло в голову, что это технически возможно. В том случае, если вспомнить, как реализованы корутины в питоне (это генераторы), и воспользоваться этой особенностью реализации. Для нетерпеливых - вот ссылка на gist с кодом.

Как же можно воспользоваться тем, что корутины - это генераторы? Приведу простой пример:

async def hi():
    print('hi')

Несмотря на то, что перед нами асинхронная функция, не обязательно иметь event loop, чтобы запустить её. Можно сделать так:

try:
  gen = hi()
  gen.send(None)
except StopIteration:
    pass

Будет напечатано hi.

У контекстного менеджера fibers - такой же принцип. Он устроен так, что весь код внутри него можно выполнить вызовом gen.send() (вот эта строчка). Дальше, мы оборачиваем этот вызов в гринлет - и применяем наш обычный подход.

Вы знаете, как выглядит простейший Awaitable? Вот один из вариантов, не самый типичный:

class Simple:
    def __await__(self):
        return 'hi'
        yield

await Simple()вернёт'hi'.

__await__- это генератор. В отличие от корутин, он может делать как yield, так и yield from. Тогда как корутины могут делать только await, что соответствует yield from.

Контекстный менеджер fibers тоже использует кастомные Awaitable. Они делают yield специальных значений start и end. Event loop их не поймёт. Поэтому нам нужно обернуть какую-нибудь корутину (например, верхнего уровня), чтобы она не пропускала их наружу. Сделать это относительно несложно: например, если у Вас - ASGI приложение, то можно обернуть это приложение целиком.

Иллюстрацию подхода можно посмотреть в gist. По сути, это просто ещё один "фронтенд" к библиотеке greenhack: для него даже не потребовалось вносить в неё изменения. Удобный API для того же самого, больше ничего.

Когда всё будет готово? Всё упирается в асинхронный бэкенд для django. Он готов, но не протестирован: нужно пройти testsuite для бэкендов django. Там около 70 тестов - когда они станут проходить, проект будет готов к продакшну. Обещать по срокам ничего не буду, но буду писать недельные отчёты (нет, не на хабре - на гитхабе). К сожалению, рабочие проекты не связаны с этой темой.

Кстати, названием проект обязан языку Ruby, точнее, Ruby Fibers. Дело в том, что в Ruby нет async/await, для асинхронности там в принципе не используются "функции другого цвета". Так достигается совместимость блокирующего кода с асинхронным - от которой в питоне отказались. К лучшему или нет - я не берусь сказать. Но я знаю, как это "исправить" - для тех случаев, когда эта совместимость нужна.

Комментарии (0)