Two types of futures

Adam Warski

22 Jun 2023.6 minutes read

Two types of futures webp image

Futures and promises have made their way into most of the mainstream languages. We intuitively know what a future represents, but what's the precise definition? Are the concepts and mechanics behind futures/promises always the same, or are there some differences?

Let's reach out to Wikipedia for a start; Wikipedia articles on Computer Science vary in quality, but this one is pretty good. Here's how a Future is described:

In computer science, future, promise, delay, and deferred refer to constructs used for synchronizing program execution in some concurrent programming languages. They describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is not yet complete.

Using simpler terminology, a Future represents a value which will be available at some point … in the future. So far, so good.

Are all futures the same?

We've reviewed the basics. But what are the two types of futures mentioned in the title?

My thesis is that the broad notion of a "future" often covers two similar but distinct concepts, and it might be helpful to differentiate between them on the terminology level.

First, we've got what I'll call "promise futures". This type of futures occurs when there's an explicit "complete" method on the future object. It's used to provide the value to which the future should resolve (once it is ready).

The read & write sides are often separated between distinct Future and Promise types. Future allows one to read the value in a blocking way, add a callback that is invoked once it is available, or sequence another Future computation (this is known as a flatMap). Promise is the write side, which allows completing the future with the ready value.

When dealing with "promise futures", we don't really know how, or if at all, the future value is being computed. It might be computed actively in the background; or the associated promise might be put somewhere in a queue, awaiting for a resource; or passed from computation thread to computation thread, slowly accumulating the data needed to resolve it.

The second type of futures are "thread futures". These futures represent a thread (might be a kernel thread, a green thread, or something in-between) running the computation to calculate the value. Once the computation completes, the future completes as well. This future-thread might block and await the availability of external resources as well, but the lifecycle of the future is clearly linked to the thread's lifecycle.

By the way, the mentioned Wikipedia article also introduced this distinction by singling out a subclass of all futures using the name "thread-specific futures".

What can you do with both types of futures?

The distinction would be moot if both promise-futures and thread-futures behaved the same, as viewed from the "read" side (where we can depend on the result somehow).

However, there's one crucial difference: cancellation. In many cases, there's a possibility to cancel or interrupt the thread, computing a value before it's ready. This might be because the value is no longer needed—maybe a timeout occurred or the originally requesting user disconnected. That allows us to save resources, propagating the interruption further on and avoiding unnecessary computation.

On the other hand, we can't cancel promise-futures. We don't know how the value is being computed, where, or if at all—so there's nowhere that our cancel signal could end up.

To be precise, some promise-future implementations expose a cancel operation, which sets a flag. When such a canceled future is attempted to be completed through the promise, an exception is thrown—signaling that the value is no longer needed. While this does allow some cleanup to be done, it doesn't prevent the value from being calculated in the first place.

It's also possible to check if a promise-future isn't canceled proactively, possibly avoiding unnecessary computation. However, this requires a lot of cooperation from the code, which calculates the future value. Hence, this is only useful in special situations requiring careful hand-crafting.

How are these concepts represented in various languages?

The distinction described above isn't anything novel. Libraries and languages distinguish between thread-futures and promise-futures to a varying degree:

  • In Java, there's a base Future type, which is used to represent any kind of future. If we submit a task to an ExecutorService, we'll get back a thread-future of type Future. The CompletableFuture type, a subclass of Future, corresponds to our notion of promise-futures. The base Future type contains a cancel method; its semantics depend on the type of the future it represents. It might interrupt the underlying thread, or it might just signal an intent to cancel.
  • In JavaScript, we only have promise-futures. Confusingly, the read-side is called a Promise, while the write-side is made available when constructing the promise.
  • In Scala, the standard library contains a promise-future implementation going by the names Future and Promise; they are non-cancellable.
  • Also in Scala, using functional effect systems, such as cats-effect or ZIO, we've got Fibers, which represent thread-futures. There are also distinct types representing promise-futures (called Promise).
  • Completing the Scala picture, the so-called "Twitter futures", are cancellable. Their design departs from what we've described above, as the "read-side" can propagate an interrupted exception to the "write-side". Thus the communication is bi-directional. Promises can register interrupt handlers, which allows running cleanup code. Additionally, interrupts automatically propagate through the flatMap chain.
  • In Kotlin, we've got the Deferred (thread-futures) and CompletableDeffered (promise-futures) types, which are similar in design to Java's Futures. They can be cancelled, but what this means depends on the sub-type

As you can see, we are far from having a standard set of names, not to mention common semantics. Some approaches use the same hierarchy for both types of futures. In others, the hierarchies are distinct. Support for cancellation varies as well.

Cancellation matters

As indicated in the introduction, I think designs which combine both future concepts into a single type might be misleading. Hence a better design could have distinct types representing promise-futures and thread-futures.

Having a Java Future, you don't know which type it is (promise- or thread-future), so you don't know what to expect from calling .cancel. Will it have any influence on the computation? Will it run to completion, or will a thread interrupt happen? Not only is this confusing, but we have no way of enforcing in code that, e.g., a cancellable thread-future is required.

Promise-futures and thread-futures are superficially similar; they both represent a value that will be available in the future, but how this value is transmitted and computed differs.

That's why I'd opt for a design where both concepts are distinct, top-level types. They still might implement a common trait, such as Awaitable or Async, if it would be useful (which might be the case, especially in asynchronous libraries; I'm not sure if in synchronous ones even this would have use-cases.)

In Ox

The reason I'm exploring this problem space is the design of Ox: an experimental concurrency library based on Java's Project Loom. Currently, Ox has a Fork type representing thread-futures, and uses Scala's standard Futures for promise-futures. The two types are entirely unrelated.

Another similar experiment, Async from EPFL, uses a single Future concept for both, with two private implementations (RunnableFuture and Promise.Future).

Which approach is better? It would be great to know your opinion—let us know—either here or in our community forum.

Summing up

We've defined two types of futures: promise-futures and thread-futures, and described how their computational model differs. While both represent values that might be available in the future, the computational process of the first is unknown and unreachable. In contrast, the second represents a thread of computation happening in the background.

This distinction is important when it comes to implementing cancellation. With promise-futures, cancellation, if available, is just a signal. With thread-futures, cancellation can cause an interruption of the thread running the computation.

Because of these differences, I'd argue that we should have distinct types for representing both concepts. Promise-futures and thread-futures seem similar, but how and when they are used varies.

Blog Comments powered by Disqus.