При использовании двух ветвей обработки одного итератора память растёт по мере отставания одной ветви: какой механизм itertools.tee это объясняет?
itertools.tee кэширует элементы, которые уже прочитала опережающая ветвь, но ещё не запросила отстающая. Поэтому дополнительная память растёт примерно пропорционально расстоянию между позициями итераторов, а не обязательно общему размеру источника.
itertools.tee предназначен для получения нескольких независимых итераторов из одного одноразового источника. Это решает проблему повторного обхода генератора или другого итератора без повторного чтения файла, запроса к сети или выполнения дорогих вычислений.
Такое разделение невозможно получить простым созданием двух ссылок на один итератор: обе ссылки будут совместно продвигать одну позицию. tee добавляет промежуточное хранение уже полученных элементов.
Если одна ветвь обработки работает значительно быстрее другой, tee вынужден сохранять все элементы, которые быстрая ветвь уже потребила, но медленная ещё не запросила. При длительном отставании это приводит к существенному росту потребления памяти и может превратить потоковую обработку в фактически буферизованную.
Проблема особенно опасна для бесконечных или очень больших источников: пока отстающая ветвь не догонит быструю, буфер не сможет освободить соответствующие элементы. Ограничение памяти процесса само по себе не заставляет tee сбрасывать эти данные.
Внутренне tee сохраняет значения, извлечённые из исходного итератора. Когда очередной дочерний итератор запрашивает элемент, он либо получает уже сохранённое значение, либо инициирует чтение следующего значения из исходного источника. Значение можно удалить из внутреннего буфера только после того, как его запросили все дочерние итераторы, которым оно предназначено.
В этом примере fast опережает slow примерно на миллион элементов, поэтому tee должен удерживать пропущенные slow значения. Точная внутренняя организация буфера не является API-контрактом, но требование хранить отставшие элементы следует из семантики независимых итераторов.
Если обе ветви должны обработать каждый элемент, но могут работать с разной скоростью, безопаснее рассмотреть однопроходную диспетчеризацию: прочитать элемент один раз и передать его обеим обработчикам. Это не устраняет очереди, если обработчики асинхронны, но позволяет явно задать их ограниченный размер и политику противодавления.
Материализация источника в список обычно проще, но требует памяти для всех элементов независимо от текущего отставания. Повторное чтение источника экономит память, однако возможно только для воспроизводимого источника и может повторить дорогие вычисления или операции ввода-вывода.
В сервисе одна ветвь разбирала записи журнала быстро, а вторая отправляла их во внешнюю систему с переменной задержкой. Использование tee выглядело удобным: каждая ветвь получила независимый итератор, но при замедлении внешней системы внутренний буфер начал расти.
Рассматривались три варианта: сохранить все записи в список, повторно читать журнал или заменить tee явной очередью между этапами. Список был отвергнут из-за неограниченного пикового потребления памяти, повторное чтение — из-за стоимости и невозможности надёжно повторить источник.
Выбрали явную ограниченную очередь с противодавлением: когда очередь заполняется, чтение новых записей приостанавливается. Это ограничило память заранее заданным буфером и сделало замедление внешней системы наблюдаемым через метрики длины очереди и времени ожидания.
1. Вопрос: Освобождает ли tee уже обработанные элементы сразу после того, как их потребила быстрая ветвь?
Нет. Элемент можно освободить только после того, как его потребили все дочерние итераторы, которые от него отстают. Если одна ветвь остановилась навсегда, её непрочитанная часть продолжит удерживаться, пока сами итераторы tee или связанные с ними структуры не станут недостижимыми.
2. Вопрос: Уменьшится ли буфер tee, если медленная ветвь временно догонит быструю?
Да. По мере чтения отстающей ветвью старые элементы перестают быть нужны всем ветвям и могут быть удалены из внутреннего буфера. Поэтому важен не максимальный размер источника, а максимальное расстояние между позициями дочерних итераторов.
3. Вопрос: Почему замена tee на список не устраняет проблему памяти?
Список меняет характер хранения, но не устраняет его: он заранее удерживает весь набор элементов. tee обычно выгоднее, когда ветви идут почти синхронно и отставание мало; список может быть проще, если набор данных заведомо небольшой и нужен многократный произвольный доступ. Для больших потоков с непредсказуемым отставанием лучше применять явно ограниченный буфер или архитектуру с противодавлением.