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 устроен сложнее лишь в деталях. Он потокобезопасен: внутри замок, и состояние меняется атомарно — PENDING → RUNNING → FINISHED (или 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 и возбуждается при чтении результата, поэтому не теряется.