Asynchronous programming is tightly linked to the concept of continuations, registering a function with surrounding closure as a callback when some operation completes successfully or fails. Although it is possible to do this ad hoc, reinventing the wheel with a slightly different shape for each use, the pattern of providing callbacks in order to handle some future results has become common and uniform enough to deserve its own concept in most programming language. Indeed, the word Future is often used to describe exactly this concept.
Imagine writing the code for asynchronously downloading a file using callback style asynchronous programming. Not too bad, right? Depending on the language you may end up with some ugly anonymous types and explicit state, or you may be able to use anonymous functions and stateful closures – either way you pretty much know how to get it right. Now imagine downloading n files in parallel, or downloading a file from n locations using only the first one that completed, or retrying a download n times if it failed before giving up completely. Like the look of the code you had to write?
Requirements for the Future
Having en entity explicitly representing the future allows you to solve patterns like the above once, correctly, then just applying the building blocks you need when you need them. Let us take a look at what other qualities we may want from such an entity before we dig into the way it has been implemented on the various platforms. Our ideal Future should strive to meet the following requirements:
- Composable – as discussed above, this is the may reason for making the future a thing rather than a pattern. Composable means that our Future should be transformable using chaining, filtering, multiplexing and demultiplexing, with each operation generating a new Future just as generic as the first.
- Minimal – following the Principle of least privelage, the Future should contain only operations required for its purpose – accessing and transforming a future result (or error). This means, as an example, that the action of completing the Future should be separate from the Future itself.
- Thread safe – while somewhat irrelevant in a single threaded environment, Futures will be used for concurrent as well as asynchronous programming where multithreading is supported. Thread safe means that all consumers must see the same result of the Future, and that the order in which the Future gets a result and the result being accessed must not affect the semantics. Thread safe also holds for the producing side, there may be a race between for example setting the result of the Future and marking it as Cancelled, our implementation must ensure that, once set, the Future keeps its result.
- Composable – the deferred allows for chaining and error handling by adding callbacks for successful results, failures or both. The pipe function allows for transformations of resulting values, either using a synchronous or asynchronous function. The built in when function allows for hassle free fork/join patterns where a number of asynchronous operations need to complete before another operation can be initiated. Conclusion: Excellent.
- Minimal – Although the jQuery Deferred is a read/write object that allows for the result of the Future to be set by anyone, jQuery also provides a read only view called Promise that only allows for the result of the future to be observerd. Conclusion: Excellent.
Futures in .NET:
Microsoft, true to form, introduced a completely new name for the Future in .NET 4, called a Task. Tasks can be created from operations running on a background thread using a task factory, or any asynchronous operation by using creating a task from a Task Completion Source. Tasks allow for continuation callbacks to be registered and allow for fine grained control over how (same thread or using a scheduler). In addition, it is possible to block until a result is available, optionally providing a timeout. The .NET task implementation has rich support for cancellation, decoupling the ability to trigger cancellation from both the task itself and the operation being cancelled. As for our requirements:
- Composable – continuations for all the completion modes allow for sane composability. In addition, a Task provides a WaitHandle that allows for Select style wait operations over multiple tasks. Built in support allows for combining multiple tasks and waiting for one out of many tasks to complete. C# 5 takes Tasks to a completely different level with built in language support for writing asynchronous code in a traditional imperative style and having the compiler do the heavy lifting. Conclusion: Excellent.
- Minimal – the .NET task does suffer from some compatibility issues, having to implement a WaitHandle and carry asynchronous state to support the older IAsyncResult interface used in the Begin/End pattern. Setting the result of the Task is nicely separated from consuming it, but Task also carries two really ugly additions – Start() and RunSynchronously(). While these make some sense for CPU bound operations meant to execute on a background thread, scheduling should be a separate concern from consuming a Future, and the methods are completely meaningless for asynchronous IO operations. Conclusion: Acceptable.
- Thread safe – the Task is safe to use in a multithreaded environment, all threads are guaranteed to see the same final state and there is no dangerous race between adding continuations and generating a result. The Task Completion Source even provides TrySet method that both safely updates the state of the related Task but also reports back whether or not a state was accepted, allowing for tasks to be cancelled from a thread different from the one producing a result. Conclusion: Excellent.
Futures in Java
- Composable – not really, the Java Future only allows for blocking until a result is generated or polling for a result, neither which allows operations to compose gracefully. The Java Future is acceptable for fork/join parallelism where a number of results need to be collected at the end of an operation, but not much more, and definitely not asynchronous IO operations. Conclusion: Poor.
- Minimal – although too minimal in some respects, the Java Future adds cancellation support. Cancellation of Futures can be done well, and has been in the .NET Task and CancellationToken, but allowing everyone with (what should be) a read only view of a future result to cancel the operation (affecting all other consumers), is a clear violation of the Separation of Concerns principle as well as the Principle of least privelage. Conclusion: Poor.
- Thread safe – java.util.concurrent.Future is an interface and as such can’t guarantee correct behavior of an implementation. The implementation provided by the framework, FutureTask, is however thread safe when it comes to concurrent updates. Conclusion: Acceptable.
- Composability – the new scala Future allows for blocking as well as continuation passing operations and has a rich set of functions for composing futures, treating Future as a monad. The Scala Traits concept allows for a rich set of higher order operations on futures accessible directly on the type without requiring more than a handful of operations to be explicitly implemented by a custom Future. While the traits will not be as nice to use from Java when implementing custom Futures, a Promise can be used to create one and (eventually) give it a result. Conclusion: Excellent.
- Minimal – although providing a rich set of methods for operating on Futures, there is a clean separation of concern and the Future is a pure read only view of an eventual result. The new Scala Future has no concept of cancellation however, which means that cancellation will be built in a number of different flavours on top of this construct. Conclusion: Good.
- Thread safe – when tasks are created from an accompanying Promise, the Future as well as the Promise itself is fully thread safe. The Promise allows for tryset operations that allow for multiple threads to attempt to set a status concurrently with predictable safe semantics. The Future being a Trait (or interface when exposed to Java) means however that the consumer of a Future cannot know with full certainty that the Future will follow sane multi threaded semantics. Conclusion: Good.