- PVSM.RU - https://www.pvsm.ru -

Синхронный код в асинхронном Twisted, или сказ о том, как скрестить ежа с ужом

Всё хорошо

Twisted [1] — асинхронный (событийно-ориентированный) фреймворк, написанный на Python. Мощное средство для быстрой разработки сетевых (и не только) сервисов. Он разработан с использованием паттерна проектирования Reactor [2]. Сервисы созданные с использованием Twisted быстры и надежны, фреймворк позволяет не писать макаронный код, насыщенный непонятными коллбэками, имеет внутри себя красивые хелперы (Deferred, Transport, Protocol etc). Другими словами, делает нашу жизнь бекенд разработчиков лучше.

Но есть и проблемы

Основная проблема в том, что многочисленные, надежные, оттестированные, удобные библиотеки, использующие в своей основе синхронные модули Python (socket, os, ssl, time, select, thread, subprocess, sys, signal etc), просто возьмут и заблокируют нам основной процесс, цикл реактора и наступит беда. Такими библиотеками, к примеру, являются psycopg2, request, mysql и другие. В частности, psycopg2 используется в Django ORM как один из бекендов баз данных.

Что же делать?

Есть три пути. Сложный, приемлемый и хороший. Сложный — реализовать аналог библиотеки на Twisted. Приемлемый — использовать deferToThread и запускать синхронный код в отдельных потоках (используя пул потоков реализованный в Twisted). О хорошем пути (по моему мнению) и пойдет речь в заметке.
Скрестить ежа с ужом [3]

Используем «зеленые» потоки и события для переключения контекста!

Что нам для этого нужно?
  • Greenlets — легковесные «зеленые» потоки, которые работают внутри главного процесса приложения
  • Gevent — фреймворк, который позволяет переключать контекст между гринлетами, в тот момент, когда исполняемый код блокируется
  • Метод реактора [deferToGreenlet], позволяющий обернуть гринлет в Deferred
Пример применения технологии в реальном проекте

Я не стал писать собственную реализацию реактора с возможностью отправлять код в гринлеты, так как нашел готовое решение, протестировал и внедрил в проект. Код реактора можно забрать отсюда [4].

Для использования geventreactor при инициализации приложения нужно его установить:

from geventreactor import install
install()

Теперь нам доступны новые методы:

__all__ = ['deferToGreenletPool', 'deferToGreenlet', 'callMultipleInGreenlet', 'waitForGreenlet', 'waitForDeferred',
           'blockingCallFromGreenlet', 'IReactorGreenlets', 'GeventResolver', 'GeventReactor', 'install']

По аналогии с reactor.deferToThread(f, *args, **kwargs), можно вызывать reactor.deferToGreenlet(f, *args, **kwargs), где f — callable объект, а *args и **kwargs его аргументы.

Чтобы все заработало необходимо также пропатчить библиотеки в пространстве имен:

from gevent import monkey

monkey.patch_all()

После данных манипуляций, основные библиотеки Python будут пропатчены фреймворком Gevent. Смотрите документацию по Gevent [5]

Теперь все библиотеки или код, который напрямую импортирует их, при вызове блокирующихся методов или функций, будут вызывать соответствующие события в системе Gevent. На эти события повешены коллбеки, позволяющие переключать контекст между гринлетами.

У меня в проекте используется Django ORM для манипуляции данными в PostgreSQL. Поэтому для того, чтобы методы ORM не блокировали процесс нужно использовать специальный бекенд, позволяющий создавать пул соединений с БД и переключаться между соединениями. Одним из бекендов является django-db-geventpool [6]

Использовать django-db-geventpool не трудно. Достаточно следовать документации.

Что дальше?

Метод reactor.deferToGreenlet возвращает объект Deferred, с которым можно работать как с обычным Deferred.

Например, у нас есть модель:

class ExampleModel(models.Model):
    title = models.CharField(max_length=256)

Мы хотим получить все модели и передать их какому-то обработчику внутри системы. Мы можем написать что-то вроде:

d = reactor.deferToGreenlet(ExampleModel.objects.all)

И наш код не заблокирует основной процесс. Ведь в тот момент, когда Django ORM вызовет cursor.execute(), который будет ожидать ответ от драйвера базы данных, geventreactor переключит контекст на другой Deferred.

Что в итоге?

Мы можем выполнять синхронный код внутри Twisted, не создавая при этом лишних потоков или процессов, при этом не блокируя event loop реактора. Главное следовать основным принципам работы с асинхронными системами, куски кода не должны выполняться слишком долго, gevent позволяет принудительно переключать контекст из любого места кода, там, где это нам удобно, достаточно лишь вызвать gevent.sleep().

Автор: viatoriche

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/106560

Ссылки в тексте:

[1] Twisted: https://twistedmatrix.com/trac/

[2] Reactor: https://en.wikipedia.org/wiki/Reactor_pattern

[3] Image: https://www.pvsm.ru/post/266887/

[4] отсюда: https://gist.github.com/yann2192/3394661

[5] Смотрите документацию по Gevent: http://www.gevent.org/gevent.monkey.html#module-gevent.monkey

[6] django-db-geventpool: https://pypi.python.org/pypi/django-db-geventpool

[7] Источник: http://habrahabr.ru/post/266887/