Futures

Futures are a more convenient pattern that can be used to keep track of the results of asynchronous calls. In the preceding code, we saw that rather than returning values, we accept callbacks and pass the results when they are ready. It is interesting to note that, so far, there is no easy way to track the status of the resource.

A future is an abstraction that helps us keep track of the requested resources and that we are waiting to become available. In Python, you can find a future implementation in the concurrent.futures.Future class. A Future instance can be created by calling its constructor with no arguments:

    fut = Future()
# Result:
# <Future at 0x7f03e41599e8 state=pending>

A future represents a value that is not yet available. You can see that its string representation reports the current status of the result which, in our case, is still pending. In order to make a result available, we can use the Future.set_result method:

    fut.set_result("Hello")
# Result:
# <Future at 0x7f03e41599e8 state=finished returned str>

fut.result()
# Result:
# "Hello"

You can see that once we set the result, the Future will report that the task is finished and can be accessed using the Future.result method. It is also possible to subscribe a callback to a future so that, as soon as the result is available, the callback is executed. To attach a callback, it is sufficient to pass a function to the Future.add_done_callback method. When the task completes, the function will be called with the Future instance as its first argument and the result can be retrieved using the Future.result() method:

    fut = Future()
fut.add_done_callback(lambda future: print(future.result(), flush=True))
fut.set_result("Hello")
# Output:
# Hello

To get a grasp on how futures can be used in practice, we will adapt the network_request_async function to use futures. The idea is that, this time, instead of returning nothing, we return a Future that will keep track of the result for us. Note two things:

  • We don't need to accept an on_done callback as callbacks can be connected later using the Future.add_done_callback method. Also, we pass the generic Future.set_result method as the callback for threading.Timer.
  • This time we are able to return a value, thus making the code a bit more similar to the blocking version we saw in the preceding section:
    from concurrent.futures import Future

def network_request_async(number):
future = Future()
result = {"success": True, "result": number ** 2}
timer = threading.Timer(1.0, lambda: future.set_result(result))
timer.start()
return future

fut = network_request_async(2)
Even though we instantiate and manage futures directly in these examples; in practical applications, the futures are handled by frameworks. 

If you execute the preceding code, nothing will happen as the code only consists of preparing and returning a Future instance. To enable further operation of the future results, we need to use the Future.add_done_callback method. In the following code, we adapt the fetch_square function to use futures:

    def fetch_square(number):
fut = network_request_async(number)

def on_done_future(future):
response = future.result()
if response["success"]:
print("Result is: {}".format(response["result"]))

fut.add_done_callback(on_done_future)

The code still looks quite similar to the callback version. Futures are a different and slightly more convenient way of working with callbacks. Futures are also advantageous, because they can keep track of the resource status, cancel (unschedule) scheduled tasks, and handle exceptions more naturally.