Pull to refresh

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

Reading time 3 min
Views 10K
Всё хорошо

Twisted — асинхронный (событийно-ориентированный) фреймворк, написанный на Python. Мощное средство для быстрой разработки сетевых (и не только) сервисов. Он разработан с использованием паттерна проектирования Reactor. Сервисы созданные с использованием 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). О хорошем пути (по моему мнению) и пойдет речь в заметке.
Скрестить ежа с ужом


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



Что нам для этого нужно?


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


Пример применения технологии в реальном проекте


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

Для использования 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

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

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

Использовать 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().
Tags:
Hubs:
+4
Comments 12
Comments Comments 12

Articles