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 — по мере готовности.
Проверьте себя
1. Чем удобен concurrent.futures по сравнению с ручным управлением потоками?
AОн работает быстрее любого другого кода
BОдин и тот же код работает с потоками и процессами, меняется лишь класс исполнителя
CОн полностью убирает GIL
DОн не требует функций
2. Что представляет собой объект Future?
AНовый процесс ОС
BОбещание результата, который будет готов позже
CКопию событийного цикла
DСписок всех потоков
3. Какой исполнитель выбрать для CPU-bound вычислений?
AThreadPoolExecutor
BProcessPoolExecutor
Cоба одинаково подходят
Dни один не подходит для CPU
Поддержать проект