greenlet/gevent

Тут будет немного сумбурных мыслей по поводу greenlet’ов в контексте gevent.

Гринлеты это хак

Немного пояснений о том как же работают гринлеты и к чему это все может привести.

В питоне весь стек - unmanaged. Это означает, что используется “обычный” стек, который предоставляет операционная система вместе со своим менеджментом стековой памяти (дополнительное выделение страниц под стек если нужно еще и так далее). В этом самом unmanaged стеке хранятся всякие локальные переменные питоновского интерпретатора, локальные переменные сишных модулей и тому подобное. Такая вот солянка из всего сразу.

Поскольку питон - интерпретируемый язык и если бы у него был управляемый стек (стек програмы на питоне был бы реализован сам интерпретатором), то greenlet’ы делаются элементарно - заводим два стека и все.

А поскольку стек как раз не управляется питоном, то greenlet начинает шаманить с системным стеком. А именно:

1. При переключении гринлетов (из А в Б) вычисляет размер использованого стека относительно вышестоящего стек-фрейма
2. Копирует данную область из стека в кучу (делая malloc/realloc)
3. Переписывает данные из кучи в стек для гринлета Б
4. Патчит регистры процессора, что бы указатель стека был на правильном месте
5. Освобождает память в куче для гринлета Б (free)
6. Патчит PyThreadState (переменные recursion_depth, top_frame) что бы питон не сломался
7. Возвращает управление в интерпретатор питона

И того, на одно переключение гринлета происходит 2 memcpy, возможно один realloc и один free. Зачем free? Потому что при алгоритмах с рекурсией размер стека может быть большим. Например - 50 КБ. Соответственно если после переключения оставлять копию стека в куче - получим 2х кратное увеличение использования памяти.

Какие общие проблемы у такого подхода:

1. Проблемы со сборкой мусора

Из официальных док: Greenlets do not participate in garbage collection; cycles involving data that is present in a greenlet’s frames will not be detected. Storing references to other greenlets cyclically may lead to leaks.

Что означает - любые переменные которые были выгружены гринлетами в кучу из стека не будут принимать участия в сборке. А поскольку у питона обычный reference count, то и все объекты на которые они ссылаются тоже подчищены не будут. 

Так что прийдется очень внимательно писать код, а то потом прийдется ловить ошибки на продакшене.

2. Вызов функции в питоне обойдется дешевле чем переключение гринлета. Возможно поэтому tornado в тестах был чуть-чуть, но быстрее чем gevent.

3. Магия для интерпретатора

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

Гринлеты занимаются магией и влезают в стек. Если произойдет исключение будет очень трудно посмотреть реальный стек-трейс.

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

Реальный размер используемого системного стека может быть значительно больше чем значение PyThreadState.recursion_depth - при слоеном пироге из фреймов разных гринлетов. Чем грозит - не знаю, памяти сейчас много, но может себя вести странно на “нестандартных” (не х86) архитектурах.

К слову, текущая интеграция для x64 нарушает конвенции вызовов функций (ABI) - не сохраняет все нужные регистры при переключении гринлетов. Так что я бы не советовал их использовать на х64 - можно словить кучу ошибок.

Вот тут еще есть немного: http://code.google.com/p/coev/wiki/GreenletProblems

Ну а теперь о хорошем.

Мне нравится концепция асинхронного программирования без callback’ов. Код становится сильно проще и красивее.

gevent, в целом - симпатичный. Ну да, нужно патчить все что можно, что бы библиотека питона стала асинхронной. Некрасиво, но других вариантов нет.

Например взять ту же SQLAlchemy - ее заставили работать асинхронно под gevent. Ага, monkey patching, но какие есть альтернативы, учитывая что это самая вменяемая ORM и она, к сожалению, не умеет работать асинхронно из коробки?

Что плохо - основа на которой написан gevent меня пугает, поскольку мне страшно ставить ее на высоконагруженный продакшн. Слишком много магии - очень плохо. Хотя вроде spotify что-то там писал на gevent, может библиотека уже production ready.

Ну а какие есть альтернативы?

Торнадо, да. Но появляется требование работы с базой данных и привет. Без баз данных - магии нет, код проще, но callback’и.

Такие вот дела.

blog comments powered by Disqus