Короткий ответ
ThreadPoolExecutor использует потоки в одном процессе и подходит для I/O-bound задач; ProcessPoolExecutor использует отдельные процессы с собственным GIL каждый, подходит для CPU-bound задач, но добавляет накладные расходы на межпроцессную передачу данных.
Как это работает подробнее
Оба предоставляют одинаковый интерфейс (submit, map, Future), но ThreadPoolExecutor создаёт threading.Thread внутри процесса, а ProcessPoolExecutor — отдельные процессы, между которыми аргументы и результаты сериализуются через IPC.
Пример кода
example-267.py
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time, math
def cpu_heavy(n):
return math.factorial(n)
with ThreadPoolExecutor(max_workers=4) as ex:
start = time.perf_counter()
list(ex.map(cpu_heavy, [100_000] * 4))
print("threads:", time.perf_counter() - start)
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=4) as ex:
start = time.perf_counter()
list(ex.map(cpu_heavy, [100_000] * 4))
print("processes:", time.perf_counter() - start)
# Ожидаемо: processes быстрее threads на многоядерной машине для этой CPU-bound задачиЧто использовать на практике
- Практика. Для большого числа одновременных сетевых запросов —
ThreadPoolExecutor(илиasyncio, если библиотеки поддерживают async); для тяжёлых вычислений на чистом Python без C-расширений, освобождающих GIL —ProcessPoolExecutor, если прирост от параллелизма превышает издержки передачи данных.
Подводные камни
- Риски. Используют
ProcessPoolExecutorдля задач с большим объёмом данных на вход/выход каждой задачи — издержки сериализации съедают выигрыш; используютThreadPoolExecutor, ожидая параллельного ускорения чистых Python-вычислений.