liveBook · Manning

Future

As explained in Grokking Concurrency

A future is a concurrency abstraction and placeholder representing the result of an asynchronous computation that will become available later.

Imagine ordering a burger and receiving an order number. You can find a table while the kitchen works; the number lets you collect the burger when it is ready. A future plays a similar role in a program: it is a placeholder returned by an asynchronous call while the result is still unknown. The eventual outcome may be a result or an error. section 12.6

Returning a future lets the caller continue instead of waiting for the computation to finish. When the result becomes available, it can be placed in the future, allowing waiting code to continue. That makes a future both a proxy for a result and a way to synchronize independent work. The names promise, delay, and deferred describe roughly similar ideas, though their meanings vary across languages. section 12.6

# Chapter 12/future_burger.py
from __future__ import annotations

import typing as T
from collections import deque
from random import randint

Result = T.Any
Burger = Result
Coroutine = T.Callable[[], 'Future']

class Future:
    def __init__(self) -> None:
        self.done = False
        self.coroutine = None
        self.result = None

    def set_coroutine(self, coroutine: Coroutine) -> None:                  #1
        self.coroutine = coroutine                                          #1

    def set_result(self, result: Result) -> None:                           #2
        self.done = True                                                    #2
        self.result = result                                                #2

    def __iter__(self) -> Future:
        return self

    def __next__(self) -> Result:                                           #3
        if not self.done:                                                   #3
            raise StopIteration                                             #3
        return self.result                                                  #3

class EventLoop:
    def __init__(self) -> None:
        self.tasks: T.Deque[Coroutine] = deque()

    def add_coroutine(self, coroutine: Coroutine) -> None:
        self.tasks.append(coroutine)

    def run_coroutine(self, task: T.Callable) -> None:
        future = task()                                                     #4
        future.set_coroutine(task)                                          #4
        try:                                                                #4
            next(future)                                                    #4
            if not future.done:                                             #4
                future.set_coroutine(task)                                  #4
                self.add_coroutine(task)                                    #4
        except StopIteration:                                               #4
            return                                                          #4

    def run_forever(self) -> None:
        while self.tasks:
            self.run_coroutine(self.tasks.popleft())

def cook(on_done: T.Callable[[Burger], None]) -> None:                      #5
    burger: str = f"Burger #{randint(1, 10)}"                               #5
    print(f"{burger} is cooked!")                                           #5
    on_done(burger)                                                         #5

def cashier(burger: Burger, on_done: T.Callable[[Burger], None]) -> None:   #6
    print("Burger is ready for pick up!")                                   #6
    on_done(burger)                                                         #6

def order_burger() -> Future:
    order = Future()                                                        #7

    def on_cook_done(burger: Burger) -> None:
        cashier(burger, on_cashier_done)

    def on_cashier_done(burger: Burger) -> None:
        print(f"{burger}? That's me! Mmmmmm!")
        order.set_result(burger)

    cook(on_cook_done)                                                      #8
    return order

if __name__ == "__main__":
    event_loop = EventLoop()
    event_loop.add_coroutine(order_burger)
    event_loop.run_forever()

A small Python Future starts unfinished, holds a coroutine and a result, and marks itself done when set_result is called. section 12.6

In a cooperative server, a nonblocking socket operation returns a future rather than making the server wait for data. The event loop tracks tasks that are ready and tasks waiting for I/O. When a socket event occurs, it runs the appropriate callback and can resume the waiting task with the operation’s result. This only works cooperatively: the loop cannot interrupt a task that never hands back control. section 12.8

# Chapter 12/asynchronous_pizza/async_socket.py
from __future__ import annotations

import select
import typing as T
import socket

from future import Future

Data = bytes

class AsyncSocket:
    def __init__(self, sock: socket.socket):
        self._sock = sock
        self._sock.setblocking(False)

    def recv(self, bufsize: int) -> Future:
        future = Future()

        def handle_yield(loop, task) -> None:
            try:
                data = self._sock.recv(bufsize)
                loop.add_ready(task, data)
            except BlockingIOError:
                loop.register_event(self._sock, select.POLLIN, future, task)

        future.set_coroutine(handle_yield)
        return future

    def send(self, data: Data) -> Future:
        future = Future()

        def handle_yield(loop, task):
            try:
                nsent = self._sock.send(data)
                loop.add_ready(task, nsent)
            except BlockingIOError:
                loop.register_event(self._sock, select.POLLOUT, future, task)

        future.set_coroutine(handle_yield)
        return future

    def accept(self) -> Future:
        future = Future()

        def handle_yield(loop, task):
            try:
                r = self._sock.accept()
                loop.add_ready(task, r)
            except BlockingIOError:
                loop.register_event(self._sock, select.POLLIN, future, task)

        future.set_coroutine(handle_yield)
        return future

    def close(self) -> Future:
        future = Future()

        def handle_yield(*args):
            self._sock.close()

        future.set_coroutine(handle_yield)
        return future

    def __getattr__(self, name: str) -> T.Any:
        return getattr(self._sock, name)

The AsyncSocket listing shows recv creating a Future and registering interest in a readable socket when receiving would block. section 12.8

Keeping the pizza counter open

Suppose cooking a pizza takes a long time. If the server cooks it inside a cooperative task, other customers must wait. Instead, it can send the work to a thread pool and return a future, leaving the server free to take orders. When cooking finishes, a notification lets the result be supplied. This demonstration is not production-ready: its exception handling is insufficient, and its event loop advances only on socket events. section 12.10

How Grokking Concurrency covers it

How deep it goes
Defines the placeholder and builds Python implementations for coroutine and socket coordination, but leaves production-grade exception handling unresolved.
Examples
Uses a burger order number, a Python Future class, and a pizza server that sends long work to a thread pool.
What it assumes
Introduces futures through a familiar order-number analogy, then uses coroutines, nonblocking sockets, and an event loop for the server.
What you can do after
Build it from scratch with Python coroutines and an event loop to return deferred socket results.
How it connects
Connects futures to coroutines, cooperative multitasking, event loops, nonblocking sockets, and thread pools.

Also explained in

Compare how 8 books explain future →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Grokking Concurrency. The section links point to the parts of the book it draws on.