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’и.
Такие вот дела.