Генераторы Python реализованы как объекты приостановленного фрейма (PyFrameObject), которые сохраняют свое состояние выполнения между вызовами. Когда вызывается send(value), внутренняя функция CPython gen_send_ex() помещает это значение в стек значений генератора, который затем извлекается и возвращается вызывающему выражением yield. Это отличается от начального вызова next(), который неявно передает None для подготовки генератора из его начального состояния (где f_lasti == -1) до первого выражения yield. Если send() вызывается с ненулевым значением до того, как генератор вернул значение в первый раз, CPython вызывает TypeError, потому что фрейм генератора не имеет позиции в стеке для получения значения. Эта архитектурная разница обеспечивает, чтобы двунаправленная связь начиналась только после того, как генератор достиг своего первого пункта приостановки.
Нам нужно было реализовать pipeline обработки данных с учетом обратного давления для обработки высокочастотных рыночных данных, где downstream-потребители могли динамически сигнализировать upstream-продукторам о необходимости ограничить или возобновить поток данных без потери сообщений или исчерпания памяти.
Один из рассмотренных подходов использовал потоки с ограниченными экземплярами queue.Queue между этапами пайплайна. Хотя это обеспечивало знакомую блокирующую семантику и безопасность потоков, оно страдало от серьезной конкуренции за GIL и накладных расходов на переключение контекста, что потребляло 15% ЦП исключительно на координацию при высоком уровне пропускной способности и добавляло непредсказуемые скачки задержки.
Другой альтернативой было миграция на asyncio корутины и синтаксис async/await. Это бы устранило конкуренцию за GIL, но потребовало полной переработки нашей синхронной библиотеки численного анализа в совместимую с async, создавая вирусное рефакторинг, которое затронуло бы тысячи строк бизнес-логики и вызвало бы проблемы совместимости с наследуемыми C расширениями.
В конце концов, мы выбрали кооперативный подход многозадачности на основе генераторов с использованием send() для передачи "кредитов спроса" вверх по потоку. Это решение полностью устранило накладные расходы GIL, не требовало переработки библиотек, так как генераторы работают в синхронном коде, и обеспечивало явный поток управления через шаблоны demand = (yield data_chunk), позволяя downstream-потребителям немедленно приостановить upstream-производство, отправляя нулевые значения.
Результатом стало снижение использования памяти на 40% по сравнению с подходом на основе очереди, задержка стабилизировалась ниже 5 миллисекунд, а кодовая база оставалась readable с явными точками yield, отмечающими границы приостановки.
Почему вызов send() с ненулевым значением на только что созданном генераторе приводит к Raise TypeError, и как это ограничение усиливает протокол генератора?
Когда генератор только создается, его указатель фрейма f_lasti равен -1, указывая на то, что ни один байт-код не был выполнен. Интерпретатор CPython проверяет, не был ли генератор запущен, когда вызывается send(); если переданное значение не равно None, возникает TypeError, потому что выражение yield еще не было достигнуто для обеспечения слота в стеке для значения. Это соблюдение гарантирует, что логика инициализации генератора выполняется до конца, прежде чем начинается двунаправленная связь, поддерживая инвариант, что значения поступают в генератор только в явных точках приостановки yield.
Как метод generator.close() гарантирует выполнение кода очистки внутри генератора, и чем исключение GeneratorExit отличается от обычных исключений?
Метод close() отправляет исключение GeneratorExit в генератор в его текущей точке приостановки, вызывая throw(GeneratorExit). GeneratorExit наследуется от BaseException, а не от Exception, чтобы предотвратить его захват общими обработчиками except Exception, которые могут неправильно его подавить. Если генератор перехватывает GeneratorExit и повторно генерирует его или завершает выполнение нормально, close() завершает работу без сообщения; однако, если генератор возвращает значение в ответ на GeneratorExit, CPython вызывает RuntimeError, поскольку закрывающийся генератор не должен производить новые значения. Этот механизм гарантирует, что блоки finally и менеджеры контекста в теле генератора выполняются даже во время принудительного завершения.
Какой механизм позволяет синтаксису yield from обрабатывать отправленные значения прозрачно через вложенные генераторы, и чем это отличается от ручной делегирования с использованием цикла с send()?
Синтаксис yield from делегирует не только итерацию, но и полный протокол генераторов подгенератору. Когда внешний генератор выполняет yield from subgen(), CPython преобразует send(value) вызывающего в прямую отправку в подгенератор, пока он не вызовет StopIteration (значение которого становится результатом выражения yield from). Это отличается от ручной делегации, где цикл типа for x in subgen(): yield x не может перехватить значения, отправленные во внешний генератор, чтобы перенаправить их во внутренний. Конструкция yield from по сути упрощает стек вызовов, позволяя двунаправленному потоку данных через произвольно глубокую вложенность генераторов без шаблонного кода перенаправления, при этом сохраняя правильную передачу исключений и семантику закрытия.