Future и Promise

Как одним объектом представить результат, которого ещё нет, и аккуратно его дождаться или подписаться на него.

Future (он же Promise) — объект-«коробка» для результата, который появится позже: пока вычисление идёт, future «не готов»; когда оно закончится, в future кладут значение, и все ожидающие его получают.

Зачем это нужно на практике

Когда вы запускаете долгую операцию в другом потоке или асинхронно, встаёт вопрос: как вернуть её результат? Блокировать вызывающий код нельзя — он ради того и не ждал. Возвращать «пока ничего» бессмысленно. Решение — отдать обещание результата: объект, который прямо сейчас пуст, но обязуется заполниться, когда работа завершится. Это и есть future/promise.

С таким объектом удобно работать двумя способами. Можно дождаться результата позже, вызвав что-то вроде future.result() — код тут заблокируется ровно до готовности. А можно подписаться колбэком: «когда будет готово — вызови вот эту функцию». Первый стиль удобен, когда результат нужен прямо сейчас; второй — когда вы хотите продолжить работу и среагировать потом. На future построены concurrent.futures, asyncio, промисы в JavaScript, CompletableFuture в Java — всюду одна идея.

Future своими руками

Чтобы понять механику, соберём мини-future сами. Внутри — флаг готовности, ячейка для значения и список колбэков. Метод set_result вызывает «тот, кто посчитал»: он кладёт значение и будит подписчиков. Метод add_done_callback регистрирует колбэк (а если результат уже готов — зовёт сразу). Всё детерминированно: никаких потоков, просто порядок вызовов.

class Future:
    def __init__(self):
        self._done = False
        self._value = None
        self._callbacks = []

    def set_result(self, value):        # "вычисление завершилось"
        self._value = value
        self._done = True
        for cb in self._callbacks:      # уведомляем подписчиков
            cb(value)

    def add_done_callback(self, cb):
        if self._done:                  # уже готов -> зовём сразу
            cb(self._value)
        else:
            self._callbacks.append(cb)

    def result(self):
        if not self._done:
            raise RuntimeError("результат ещё не готов")
        return self._value

f = Future()
log = []
f.add_done_callback(lambda v: log.append(f"колбэк A увидел {v}"))
f.add_done_callback(lambda v: log.append(f"колбэк B увидел {v}"))

print("Готов до вычисления?", f._done)
f.set_result(42)                        # производитель кладёт результат
print("Готов после?", f._done)
print("result():", f.result())
for line in log:
    print(line)

# поздняя подписка на уже готовый future
f.add_done_callback(lambda v: print(f"поздний колбэк сразу получил {v}"))

Вывод:

Готов до вычисления? False
Готов после? True
result(): 42
колбэк A увидел 42
колбэк B увидел 42
поздний колбэк сразу получил 42

Обратите внимание на две ветки в add_done_callback: подписка до готовности откладывается, а после — срабатывает мгновенно. Это устраняет гонку «а вдруг результат появился ровно между проверкой и подпиской».

Цепочки: promise.then

В мире промисов есть ещё один приём — композиция через then: «когда будет результат, преобразуй его этой функцией и верни новый promise». Так из значения строят конвейер преобразований, не дожидаясь каждого шага вручную. Соберём крошечный promise с then:

class Promise:
    def __init__(self):
        self.value = None
        self.ready = False
        self._next = []
    def resolve(self, v):
        self.value = v
        self.ready = True
        for fn, nxt in self._next:
            nxt.resolve(fn(v))
    def then(self, fn):
        nxt = Promise()
        if self.ready:
            nxt.resolve(fn(self.value))
        else:
            self._next.append((fn, nxt))
        return nxt

p = Promise()
final = (p.then(lambda x: x + 1)
          .then(lambda x: x * 10)
          .then(lambda x: f"итог={x}"))

p.resolve(4)                    # запускаем цепочку: 4 -> 5 -> 50 -> "итог=50"
print(final.value)

Вывод:

итог=50

Цепочку собрали до того, как появилось значение: каждый then вернул новый пустой promise. А когда вызвали resolve(4), значение прокатилось по конвейеру: 4 → 5 → 50 → «итог=50». Так промисы позволяют описывать «что сделать с результатом», ещё не имея его.

Как это работает под капотом

Настоящий concurrent.futures.Future устроен сложнее лишь в деталях. Он потокобезопасен: внутри замок, и состояние меняется атомарно — PENDINGRUNNINGFINISHED (или CANCELLED). Метод result(timeout) на незавершённом future блокирует поток на условной переменной, пока воркер не вызовет set_result и не разбудит ожидающих. Если задача упала, исключение прячут внутрь через set_exception, и оно повторно возбуждается при чтении result() — ошибка не теряется, а доезжает до того, кто ждал.

В asyncio идея та же, но без блокировки потока: await future приостанавливает корутину и отдаёт управление циклу событий; когда future завершится, цикл «возобновит» корутину с этого места. То есть future — это универсальный мост между «здесь и сейчас запросили» и «там и потом посчитали», независимо от того, кроется ли за этим поток, процесс или событийный цикл.

Частые ошибки

  • result() без таймаута на главном потоке. Если задача зависнет, вызов заблокируется навсегда. Указывайте timeout и обрабатывайте TimeoutError.
  • Забыли про исключение. Ошибка задачи спит в future и всплывёт лишь при result(). Если результат не читать, сбой пройдёт незамеченным.
  • Гонка «проверил-подписался». Сначала смотреть done(), а потом отдельно вешать колбэк — опасно: результат может прийти между этими шагами. Правильный add_done_callback сам зовёт колбэк, если future уже готов (как в нашем примере).
  • Блокирующий result() внутри asyncio. В корутине нужно await, а не синхронный result() — иначе вы заморозите весь событийный цикл.

Итоги

  • Future/Promise — объект-обещание результата, который появится позже.
  • Два способа получить значение: дождаться через result() или подписаться колбэком.
  • Правильная подписка срабатывает сразу, если результат уже готов, — это убирает гонку.
  • Исключение задачи хранится в future и возбуждается при чтении результата, поэтому не теряется.
Проверьте себя
1. Что такое future (promise) по сути?
AСпособ ускорить процессор в N раз
BОбъект-обещание результата, который ещё не готов, но заполнится позже, когда вычисление завершится
CОчередь задач для пула потоков
DСпециальный тип потока
2. Какие два основных способа получить результат из future?
AТолько напечатать его в консоль
BДождаться блокирующим result() либо подписаться колбэком, который вызовут по готовности
CПерезапустить программу или подождать таймер
DСкопировать future в новый поток
3. Что происходит с исключением, если задача за future упала?
AОно молча теряется навсегда
BПрограмма сразу падает в момент сбоя задачи
CОно сохраняется внутри future и повторно возбуждается при чтении result()
DFuture автоматически перезапускает задачу