concurrent.futures: пулы исполнителей
Единый высокоуровневый интерфейс, чтобы раздать работу пулу потоков или процессов.
Executor — пул рабочих (потоков или процессов), которому отдают задачи. Future — объект-обещание результата, который будет готов позже.
Зачем нужен concurrent.futures
Ручное управление потоками и процессами громоздко: создать, запустить, дождаться, собрать результаты, обработать ошибки. Модуль concurrent.futures прячет это за простым интерфейсом. Главное удобство — один и тот же код работает и с потоками, и с процессами: достаточно поменять класс исполнителя.
| Класс | Использует | Для каких задач |
ThreadPoolExecutor | потоки | I/O-bound |
ProcessPoolExecutor | процессы | CPU-bound |
Идея использования
Типичный сценарий: создаём исполнитель через with, раздаём задачи методом submit (получаем Future) или map (получаем результаты), забираем результаты. Этот код запускается на реальной машине; в браузерной песочнице пулы процессов недоступны, поэтому пример иллюстративный.
from concurrent.futures import ProcessPoolExecutor
def heavy_square(n):
return n * n # представьте тяжёлое вычисление
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=4) as pool:
# map: применяет функцию ко всем элементам, результаты по порядку
results = list(pool.map(heavy_square, range(6)))
print(results)
Ожидаемый вывод:
[0, 1, 4, 9, 16, 25]
Чтобы переключиться с процессов на потоки, меняется ровно одна строка — класс исполнителя на ThreadPoolExecutor. Вся остальная логика остаётся прежней. Это и есть главная ценность модуля.
submit и Future
Когда задачи разнородны, удобнее submit: он сразу возвращает Future, у которого позже можно спросить результат через .result(). as_completed отдаёт future по мере их завершения.
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=3) as pool:
futures = [pool.submit(heavy_square, n) for n in range(3)]
for fut in as_completed(futures):
print("Готово:", fut.result())
Модель Future без внешних пулов
Чтобы понять идею «обещания результата», смоделируем мини-Future на чистом stdlib. Объект хранит функцию, выполняет её при запросе результата и кеширует ответ — как настоящий Future прячет уже посчитанное значение.
class MiniFuture:
def __init__(self, func, *args):
self._func = func
self._args = args
self._done = False
self._value = None
def result(self):
if not self._done: # ленивое вычисление при первом запросе
self._value = self._func(*self._args)
self._done = True
return self._value
def square(n):
return n * n
futures = [MiniFuture(square, n) for n in range(5)]
print("Задачи созданы, ещё не посчитаны")
print("Результаты:", [f.result() for f in futures])
Вывод:
Задачи созданы, ещё не посчитаны Результаты: [0, 1, 4, 9, 16]
Настоящий Future из concurrent.futures отличается тем, что вычисление идёт в фоне (в потоке или процессе), а .result() при необходимости блокирует и ждёт. Но идея «объект-обещание, отдающее результат позже» — ровно такая.
Итог
concurrent.futuresдаёт единый интерфейс к пулам потоков и процессов.ThreadPoolExecutor— для I/O-bound,ProcessPoolExecutor— для CPU-bound; переключение в одну строку.submitвозвращаетFuture(обещание результата),map— результаты по порядку,as_completed— по мере готовности.