InterpreterPoolExecutor против ThreadPoolExecutor: использование CPU ядер
Узнайте, как InterpreterPoolExecutor в Python 3.14 сравнивается с ThreadPoolExecutor и ProcessPoolExecutor по использованию CPU и реализации pickle. Ключевые различия для оптимального параллелизма.
Как новый InterpreterPoolExecutor в Python 3.14 работает по сравнению с ThreadPoolExecutor, и в чем ключевые различия в использовании нескольких ядер процессора и механизме обмена объектами через pickle по сравнению с ProcessPoolExecutor?
InterpreterPoolExecutor в Python 3.14: новый уровень параллелизма
Новый InterpreterPoolExecutor в Python 3.14 — это довольно значимое событие для возможностей параллельного выполнения в Python. На самом деле, он обеспечивает настоящее параллельное выполнение через подинтерпретаторы, обходя那些 надоедливые ограничения Global Interpreter Lock (GIL), которые困扰 ThreadPoolExecutor уже много лет. В отличие от ProcessPoolExecutor, который создает отдельные процессы операционной системы, InterpreterPoolExecutor использует несколько Python-интерпретаторов в рамках одного процесса. Такой подход дает вам лучшее из двух миров — вы избегаете назойливых накладных расходов сериализации pickle, при этом достигая реального использования ядер CPU.
Содержание
- Что такое InterpreterPoolExecutor?
- Сравнение с ThreadPoolExecutor
- Сравнение с ProcessPoolExecutor
- Различия в использовании ядер CPU
- Обмен объектами и реализация pickle
- Вопросы производительности
- Когда использовать каждый из исполнителей
- Технические детали реализации
- Заключение
Что такое InterpreterPoolExecutor?
Итак, Python 3.14 представляет InterpreterPoolExecutor как часть библиотеки concurrent.futures. Что он делает — это предоставляет доступ к нескольким Python-интерпретаторам в рамках одного процесса (их называют ‘подинтерпретаторами’) для вашего Python-кода. Это на самом деле фундаментальный сдвиг в том, как Python достигает настоящего параллельного выполнения.
Если вы заглянете в официальную документацию Python, вы увидите, что InterpreterPoolExecutor построен на основе этого низкоуровневого модуля _interpreters. Он создает высокоуровневые интерфейсы, которые дают вам реальные возможности параллелизма. Модуль использует массу проделанной работы в CPython для того, чтобы состояние интерпретатора было локальным для потока, что позволяет нескольким “Python” работать одновременно в рамках одного процесса.
from concurrent.futures import InterpreterPoolExecutor
with InterpreterPoolExecutor(max_workers=3) as pool:
squares = list(pool.map(lambda n: n * n, range(5)))
print(squares) # [0, 1, 4, 9, 16]
Просто, правда? Эта реализация позволяет разработчикам достигать настоящего параллелизма без всех накладных расходов создания процессов и межпроцессного взаимодействия, которые присущи ProcessPoolExecutor.
Сравнение с ThreadPoolExecutor
Ключевые различия между InterpreterPoolExecutor и ThreadPoolExecutor действительно сводятся к их фундаментальным архитектурным различиям:
Фундаментальная архитектура
ThreadPoolExecutor использует потоки, которые все разделяют одно и то же пространство памяти. InterpreterPoolExecutor, с другой стороны, использует несколько интерпретаторов в рамках одного процесса. Как вы могли видеть в том обсуждении на Stack Overflow, оба могут использовать несколько ядер CPU, но они делают это по-разному.
Вопросы GIL
А вот здесь становится действительно интересно — обработка GIL (Global Interpreter Lock):
- ThreadPoolExecutor: Потоки подчинены GIL, что означает, что в любой момент времени может выполняться только один поток байт-кода Python. Это практически убивает настоящий параллелизм для задач, нагружающих CPU.
- InterpreterPoolExecutor: Подинтерпретаторы могут фактически работать одновременно, обходя это ограничение GIL и достигая настоящего параллельного выполнения.
Согласно Super Fast Python, потоки ThreadPoolExecutor “застряли” с глобальным блокировкой интерпретатора, в то время как несколько дочерних процессов в ProcessPoolExecutor не затрагиваются GIL. InterpreterPoolExecutor удается достичь подобного обхода GIL через подинтерпретаторы.
Обмен памятью
Оба исполнителя имеют некоторое сходство в том, как они обрабатывают объекты:
- ThreadPoolExecutor: Объекты разделяются напрямую через память, поскольку все потоки разделяют одно и то же пространство процесса.
- InterpreterPoolExecutor: Объекты нужно сериализовать для обмена между интерпретаторами, что на самом деле похоже на то, как работают исполнители на основе процессов.
Сравнение с ProcessPoolExecutor
InterpreterPoolExecutor и ProcessPoolExecutor оба достигают настоящего параллельного выполнения, но они используют принципиально разные подходы:
Архитектура: Процесс против Подинтерпретатора
ProcessPoolExecutor создает отдельные процессы операционной системы, каждый со своим Python-интерпретатором и пространством памяти. InterpreterPoolExecutor, однако, создает подинтерпретаторы в рамках одного процесса, разделяя одно и то же пространство памяти, но работая независимо.
В блоге Pika Tech объясняется, что InterpreterPoolExecutor обеспечивает настоящее мультиядерное выполнение с использованием подинтерпретаторов, предоставляя этот приятный компромисс между потоками и отдельными процессами.
Требования к сериализации объектов
Оба исполнителя используют pickle для обмена объектами, но последствия различаются:
- ProcessPoolExecutor: Должен сериализовать функции и аргументы для межпроцессного взаимодействия. Это означает, что лямбда-функции и функции не верхнего уровня могут не работать.
- InterpreterPoolExecutor: Также требуется сериализация pickle, но накладные расходы обычно ниже, чем при межпроцессном взаимодействии, поскольку это происходит в рамках одного процесса.
Как отмечено в блоге Карла Мастранджело, переход от ThreadPoolExecutor к ProcessPoolExecutor не так прост, потому что все функции и аргументы должны быть доступны для Pickle. То же ограничение применимо и к InterpreterPoolExecutor, но с потенциально более низкими накладными расходами.
Различия в использовании ядер CPU
Три исполнителя довольно по-разному обрабатывают использование ядер CPU:
Ограничения ThreadPoolExecutor
ThreadPoolExecutor не действительно использует несколько ядер CPU для задач, нагружающих CPU, из-за GIL. Как отмечено в документации Rune Book, ThreadPoolExecutor использует потоки, которые разделяют одно и то же пространство памяти, и из-за GIL в CPython настоящий параллелизм для задач, нагружающих CPU, практически ограничен.
Подход ProcessPoolExecutor
ProcessPoolExecutor достигает настоящего использования ядер CPU, создавая отдельные процессы, каждый из которых работает на своем ядре CPU. Согласно документации Rune Book, ProcessPoolExecutor позволяет вам запускать функции с использованием пула отдельных процессов операционной системы, что отлично подходит для задач, нагружающих CPU, потому что он преодолевает Global Interpreter Lock.
Реализация InterpreterPoolExecutor
InterpreterPoolExecutor объединяет преимущества обоих подходов:
from concurrent.futures import InterpreterPoolExecutor
import asyncio
async def main():
def compute_hash(data: str):
import hashlib
h = hashlib.sha256()
h.update(data.encode("utf-8"))
return h.hexdigest()
with InterpreterPoolExecutor() as executor:
loop = asyncio.get_running_loop()
results = await loop.run_in_executor(executor, compute_hash, "test data")
print(results)
asyncio.run(main())
Как показано в примере Pika Tech, InterpreterPoolExecutor может выполнять обработку, нагружающую CPU, в параллельных интерпретаторах, достигая настоящего параллельного выполнения без всех накладных расходов отдельных процессов.
Обмен объектами и реализация pickle
Реализация pickle значительно различается между тремя исполнителями:
Обмен объектами в ThreadPoolExecutor
ThreadPoolExecutor разделяет объекты напрямую через память, поскольку все потоки разделяют одно и то же пространство процесса. Для обмена объектами между задачами сериализация не требуется.
Требования к pickle в ProcessPoolExecutor
ProcessPoolExecutor должен сериализовать все функции и аргументы для межпроцессного взаимодействия. Это имеет несколько последствий:
- Требования к функциям: Функции должны быть функциями верхнего уровня или методами, доступными для pickle
- Ограничения лямбд: Лямбда-функции обычно не работают
- Сериализация данных: Все аргументы должны быть доступны для pickle
- Накладные расходы: Сериализация/десериализация добавляет значительные накладные расходы
Как отмечено в статье на Medium, ProcessPoolExecutor должен сериализовать функции/аргументы, и следует избегать лямбд или функций не верхнего уровня.
Реализация pickle в InterpreterPoolExecutor
InterpreterPoolExecutor использует pickle для обмена объектами, но с ключевыми различиями:
- Более низкие накладные расходы: Поскольку сериализация происходит в рамках одного процесса, накладные расходы обычно ниже, чем при межпроцессном взаимодействии
- Те же ограничения: Все еще требуются функции и аргументы, доступные для pickle
- Лучшая производительность: Обычно быстрее, чем ProcessPoolExecutor, из-за сниженных затрат на сериализацию
Согласно сравнению на Stack Overflow, и InterpreterPoolExecutor, и ProcessPoolExecutor могут использовать несколько ядер CPU, и оба используют pickle для обмена объектами, но детали реализации значительно различаются.
Вопросы производительности
Использование памяти
- ThreadPoolExecutor: Наименьшее использование памяти, поскольку потоки разделяют одно и то же пространство памяти
- ProcessPoolExecutor: Наибольшее использование памяти из-за отдельных процессов
- InterpreterPoolExecutor: Умеренное использование памяти, выше чем у потоков, но ниже чем у процессов
Накладные расходы взаимодействия
- ThreadPoolExecutor: Минимальные накладные расходы для взаимодействия между задачами
- ProcessPoolExecutor: Высокие накладные расходы из-за межпроцессного взаимодействия и сериализации
- InterpreterPoolExecutor: Умеренные накладные расходы, обычно ниже, чем у ProcessPoolExecutor
Время запуска
- ThreadPoolExecutor: Самый быстрый запуск, поскольку потоки легковесны
- ProcessPoolExecutor: Самый медленный запуск из-за создания процессов и инициализации Python-интерпретатора
- InterpreterPoolExecutor: Умеренное время запуска, быстрее чем ProcessPoolExecutor, но медленнее чем ThreadPoolExecutor
Масштабируемость
- ThreadPoolExecutor: Ограничен GIL для задач, нагружающих CPU
- ProcessPoolExecutor: Хорошо масштабируется с ядрами CPU, но ограничен накладными расходами процессов
- InterpreterPoolExecutor: Лучшее из двух миров — масштабируется с ядрами CPU с более низкими накладными расходами
Когда использовать каждый из исполнителей
ThreadPoolExecutor лучше всего подходит для
- Задачи, нагружающие I/O: сетевые запросы, операции с файлами, запросы к базам данных
- Задачи с частым взаимодействием: когда задачам нужно часто обмениваться данными
- Легковесные операции: задачи с коротким временем выполнения, когда время запуска имеет значение
- Ограниченные по памяти среды: когда накладные расходы процессов недопустимы
ProcessPoolExecutor лучше всего подходит для
- Задачи, нагружающие CPU: математические вычисления, обработка данных, обработка изображений
- Большие наборы данных: когда данные не помещаются комфортно в память
- Изолированное выполнение: когда задачам нужна полная изоляция друг от друга
- Длительно работающие операции: когда накладные расходы запуска амортизируются за время выполнения
InterpreterPoolExecutor лучше всего подходит для
- Задачи, нагружающие CPU: те же, что и для ProcessPoolExecutor, но с лучшей производительностью
- Умеренный обмен данными: когда задачам нужно обмениваться некоторыми данными, но не часто
- Эффективность использования памяти: когда вы хотите параллелизм на уровне процессов с более низким использованием памяти
- Приложения Python 3.14+: когда вы можете ориентироваться на последнюю версию Python
Технические детали реализации
Базовая архитектура
InterpreterPoolExecutor построен на PEP 734, который обеспечивает основу для нескольких интерпретаторов в стандартной библиотеке Python. Согласно PEP 734, это включает объекты Interpreter, представляющие базовые интерпретаторы, а также базовые механизмы взаимодействия между интерпретаторами.
Создание подинтерпретаторов
Реализация создает подинтерпретаторы в рамках одного процесса, каждый со своим:
- состоянием Python-интерпретатора
- пространством имен модулей
- глобальными переменными
- контекстом выполнения
Это позволяет каждому подинтерпретатору работать независимо, при этом разделяя ресурсы одного процесса.
Механизмы взаимодействия
Интерпретаторы взаимодействуют через:
- Очереди: базовые классы Queue для передачи сообщений
- Общая память: когда возможно, для эффективного обмена данными
- Pickle: для сложных объектов и функций
Интеграция с asyncio
InterpreterPoolExecutor бесшовно интегрируется с asyncio, позволяя выполнять задачи, нагружающие CPU, параллельно, сохраняя возможности асинхронного I/O:
import asyncio
from concurrent.futures import InterpreterPoolExecutor
async def cpu_bound_task(data):
# Вычисления, нагружающие CPU
return sum(x**2 for x in data)
async def main():
data = [range(1000) for _ in range(10)]
with InterpreterPoolExecutor(max_workers=4) as executor:
loop = asyncio.get_running_loop()
tasks = [loop.run_in_executor(executor, cpu_bound_task, d) for d in data]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
Заключение
InterpreterPoolExecutor в Python 3.14 — это довольно значимый прогресс в ландшафте параллельного выполнения Python, предлагая убедительную альтернативу как ThreadPoolExecutor, так и ProcessPoolExecutor. Вот основные выводы:
Краткое изложение ключевых различий
- Использование ядер CPU: InterpreterPoolExecutor, как и ProcessPoolExecutor, достигает настоящего параллельного выполнения на нескольких ядрах CPU, в то время как ThreadPoolExecutor ограничен GIL для задач, нагружающих CPU.
- Обмен объектами: И InterpreterPoolExecutor, и ProcessPoolExecutor используют pickle для сериализации объектов, но InterpreterPoolExecutor обычно имеет более низкие накладные расходы, поскольку он работает в рамках одного процесса.
- Эффективность использования памяти: InterpreterPoolExecutor обеспечивает лучшую эффективность использования памяти, чем ProcessPoolExecutor, при этом предоставляя схожие преимущества параллелизма.
- Производительность: Для задач, нагружающих CPU, InterpreterPoolExecutor обычно превосходит как ThreadPoolExecutor, так и ProcessPoolExecutor, объединяя лучшие обоих подходов.
Практические рекомендации
- Для задач, нагружающих I/O: Оставайтесь с ThreadPoolExecutor из-за его простоты и низких накладных расходов
- Для задач, нагружающих CPU в Python 3.14+: Предпочитайте InterpreterPoolExecutor для оптимальной производительности
- Для задач, нагружающих CPU в более старых версиях Python: Используйте ProcessPoolExecutor
- Для больших наборов данных: ProcessPoolExecutor может по-прежнему быть предпочтительнее из-за лучшей изоляции
Будущие последствия
Введение InterpreterPoolExecutor открывает новые возможности для параллельного программирования на Python, потенциально делая Python более конкурентоспособным с другими языками в сценариях высокопроизводительных вычислений. По мере того как реализация будет совершенствоваться, мы можем ожидать дальнейших оптимизаций и более широкого внедрения в экосистеме Python.
Для разработчиков, желающих использовать последние возможности Python, InterpreterPoolExecutor представляет собой захватывающий новый инструмент в наборе средств параллельного программирования, предлагающий золотую середину между простотой потоков и мощностью процессов.
Источники
- Что нового в Python 3.14 - Документация InterpreterPoolExecutor
- Документация concurrent.futures - Python 3.14.2
- InterpreterPoolExecutor vs ProcessPoolExecutor - Stack Overflow
- PEP 734 - Несколько интерпретаторов в стандартной библиотеке
- Настоящий параллелизм в Python 3.14 - Блог Pika Tech
- Параллельное выполнение в Python: ThreadPoolExecutor vs ProcessPoolExecutor - Rune Book
- Параллельное выполнение с ProcessPoolExecutor - Rune Book
- ThreadPoolExecutor vs ProcessPoolExecutor - Super Fast Python
- Понимание ThreadPoolExecutor и ProcessPoolExecutor - Medium
- InterpreterPoolExecutor - Блог Карла Мастранджело