11. Asynchronous
Learning Objectives
After this lecture, students should:
- familiar with the concept of asynchronous method calls and be able to use it effectively
- familiar with the concept of promise through Java 8
CompletableFuture
class
Synchronous vs. Asynchronous
In synchronous programming, when we call a method, we expect the method to be executed, and when the method returns, the result of the method is available.
1 2 3 4 5 | int multiply(int x, int y) { return x * y; } int z = multiply(3, 4); |
In the simple example above, our code continues executing after, and only after multiply()
completes.
If a method takes a long time to run, however, the execution will delay the execution of subsequent methods, and maybe undesirable.
Asynchronous call to a method allows execution to continue immediately after calling the method, so that we can continue executing the rest of our code, while the long-running method is off doing its job.
You have seen examples of asynchronous calls:
1 2 | task = new MatrixMultiplyerTask(m1, m2); task.fork(); |
The call above returns immediately even before the matrix multiplication is complete. We can later return to this task, and call task.join()
to get the result (waiting for it if necessary).
A RecursiveTask
also has a isDone()
method that it implements as part of the Future
interface. Now, we can do something like this:
1 2 3 4 5 6 7 | task = new MatrixMultiplyerTask(m1, m2); task.fork(); while (!task.isDone()) { System.out.print("."); Thread.sleep(1000); } System.out.print("done"); |
So, while the task is running, we can print out a series of "."s to feedback to the users to indicate that it is running.
Thread.sleep(1000)
cause the current running thread to sleep for 1s. It might throw an InterruptedException
, if the user interrupts the program (by Control-C). To complete the snippet, we should catch the exception and cancel the task.
1 2 3 4 5 6 7 8 9 10 11 12 | task = new MatrixMultiplyerTask(m1, m2); task.fork(); try { while (!task.isDone()) { System.out.print("."); Thread.sleep(1000); } System.out.println("done"); } catch (InterruptedException e) { task.cancel(); System.out.println("cancelled"); } |
Future
Let's look at the Future
interface a bit more. Future<T>
represents the result (of type T
) of an asynchronous task that may not be available yet. It has five simple operations:
get()
returns the result of the computation (waiting for it if needed).get(timeout, unit)
returns the result of the computation (waiting for up to the timeout period if needed).cancel(interrupt)
tries to cancel the task -- ifinterrupt
is true, cancel even if the task has started. Otherwise, cancel only if the task is still waiting to get started.isCancelled()
returnstrue
of the task has been cancelled.isDone()
returnstrue
if the task has been completed.
Both RecursiveTask
and RecursiveAction
implements the Future
interface, so you can use the above methods on your tasks.
In Other Languages
Scala's Future
is more powerful -- it allows us to specify what to do when the task completes, and it hands abnormal completions (e.g., exceptions).
Python 3.2 supports Future
through concurrent.futures
module. C++11 supports std::future
](http://en.cppreference.com/w/cpp/thread/future) as well.
CompletableFuture
The example code above tries every second to see if task is done. For some applications, the response time is critical, and we would like to know as soon as a task is done. For instance, response time is important in stock trading applications and web services.
One way to do so, is to sleep for a shorter duration. Or even not sleeping all together:
1 2 3 4 5 | task.fork(); while (!task.isDone()) { System.out.print("."); } System.out.print("done"); |
This is problematic in many ways, besides printing out too many dots:
- this is known as busy waiting -- and it occupies the CPU while doing nothing. Such code should be avoided at all cost.
- we may want to continue doing other things besides printing out "."s, so the code won't be a simple for loop anymore. We can do something like this instead:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | task.fork(); if (!task.isDone()) { // do something } else { task.join(); } if (!task.isDone()) { // do something else } else { task.join(); } if (!task.isDone()) { // do yet something else } else { task.join(); } |
You can see that the code gets out of hand quickly, and this is only if we have one asynchronous call!
What we need is have a way to specify a callback. A callback is basically a method that will be executed when a certain event happens. In this case, we need to specify a callback when an asynchronous task is complete. This way, we can just call an asynchronous task, specify what to do when the task is completed, and forget about it. We do not need to check again and again if the task is done.
To do exactly this, Java 8 introduces the class CompletableFuture<V>
, which implements the Future<V>
interface. Thus, just like Future<V>
, a CompletableFuture<V>
object returns a value of type V
when it completes. But CompletableFuture<V>
is more powerful, it allows us to specify an asynchronous task, and an action to perform when the task completes.
The notion of "complete" is important for CompletableFuture
. If the CompletableFuture
is complete, then the value to return is available. We can create an already-completed CompletableFuture
, passing in a value, or a yet-to-be-completed CompletableFuture
, by passing in a function to be executed asynchronously. When this function returns, the CompletableFuture
completes.
To create a CompletableFuture
object, we can call one of its static method. For instance, supplyAsync
takes in a Supplier
:
1 | CompletableFuture<Matrix> future = CompletableFuture.supplyAsync(() -> m1.multiply(m2)); |
As explained above, future
completes when m1.multiply(m2)
returns.
Let's say that we want to print out the result with a Consumer
when future
completes, we can use the thenAccept
method:
1 | future.thenAccept(System.out::println); |
Or, you can use the oneliner:
1 2 3 | CompletableFuture .supplyAsync(() -> m1.multiply(m2)) .thenAccept(System.out::println); |
Waiting for Completion
If you want your code to block until a CompletableFuture
completes, you can call join()
.
1 | m = future.join(); |
Suppose you have several CompletableFuture
objects, say cf1
, cf2
, and cf3
, and you want to block until all of these CompletableFuture
completes. You can create a composite CompletableFuture
objects, using allOf()
:
1 | CompletableFuture.allOf(cf1, cf2, cf3).join(); |
The object created by CompletableFuture.allOf(cf1, cf2, cf3)
completes, only after all of cf1
, cf2
, cf3
completes.
There is also a anyOf
, for cases where it is sufficient for any one of the CompletableFuture
to complete:
1 | CompletableFuture.anyOf(cf1, cf2, cf3).join(); |
CompletableFuture is a Functor / Monad
CompletableFuture
is a functor. Recall that a functor, in OO-speak, is a class that implements a (hypothetical) interface that looks like the following:
1 2 3 | interface Functor<T> { public <R> Functor<R> f(Function<T,R> func); } |
In CompletableFuture
, the method that makes CompletableFuture
a functor is the thenApply
method:
1 | <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> func) |
The method thenApply
is similar to thenAccept
, except that instead of a Consumer
, the callback that gets invoked when the asynchronous task completes is a `Function.
There are other variations:
thenRun
, which takes aRunnable
,thenAcceptBoth
, which takes aBiConsumer
and anotherCompletableFuture
thenCombine
, which takes aBiFunction
and anotherCompletableFuture
thenCompose
, which takes in aFunction
fn
, which instead of returning a "plain" type,fn
returns aCompletableFuture
.
All the methods above return a CompletableFuture
.
BTW, CompletableFuture
is a monad too! The thenCompose
method is analougous to the flatMap
method of Stream
and Optional
.
This also means that CompletableFuture
satisfies the monad laws, one of which is that there is a method to wrap a value around with a CompletableFuture
. We call this the of
method in the context of Stream
and Optional
, but in CompletableFuture
, it is called completedFuture
. This method creates a CompletableFuture
that is completed.
The completedFuture
method is useful, for instance, if we want to convert a method below to asynchronous.
1 2 3 4 5 6 | Integer foo(int x) { if (x < 0) return 0; else return doSomething(x); } |
With CompletableFuture
, it becomes:
1 2 3 4 5 | CompletableFuture<Integer> fooAsync(int x) { if (x < 0) return CompletableFuture.completedFuture(0); else return CompletableFuture.supplyAsync(() -> doSomething(x)); |
Extra Example
In the class, I got carried away with the question about completedFuture
and added the following example for flatMap
thenCompose
as well:
Original non-async version:
1 2 | int x = bar(z) int y = foo(x) |
Async version:
1 2 3 | y = barAsync(z) .thenCompose(i -> fooAsync(i)) .get(); |
When we discussed about monad, we say that one way to think of a monad as a wrapper of a value in some context. In the case of Optional
, the context is that the value may or may not be there. In the context of CompletableFuture
, the context is that the value not be available yet.
Being a functor and a monad, CompletableFuture
objects can be chained together, just like Stream
and Optional
. We can write code like this:
1 2 3 4 5 6 | CompletableFuture .completedFuture(Matrix.generate(nRows, nCols, rng::nextDouble)) .thenApply(m -> m.multiply(m1)) .thenApply(m -> m.add(m2)) .thenApply(m -> m.transpose) .thenAccept(System.out::println); |
Another example:
1 2 3 4 5 6 | CompletableFuture left = CompletableFuture .supplyAsync(() -> a1.multiply(b1)); CompletableFuture right = CompletableFuture .supplyAsync(() -> a2.multiply(b2)) .thenCombine(left, (m1, m2) -> m1.add(m2)); .thenAccept(System.out::println); |
Similar to Stream
, some of the methods are terminal (e.g., thenRun
, thenAccept
), and some are intermediate (thenApply
).
Variations
-
There are variations of methods with name containing the word
Either
orBoth
, taking in anotherCompletableFuture
. These methods invoke the givenFunction
/Runnable
/Consumer
when either one (forEither
) or both (forBoth
) of theCompletableFuture
completes. -
There are variations of methods with name ending with the word
Async
. These methods are called asynchronously in another thread
For example, runAfterBothAsync(future, task)
would run task
only after this
and given future
is completed.
Other features of CompletableFuture
include:
-
Some methods take in additional
Executor
parameter, for cases where running in the defaultForkJoinPool
is not good enough. -
Some methods takes in additional
Throwable
parameter, for cases where earlier calls might throw an exception.
Handling Exceptions
Handling exceptions is non-trivial for asynchronous methods. Remember that, in synchronous method calls, the exceptions are repeatedly thrown to the caller up the call stack, until someone catches the exception. For asynchronous calls, it is not so obvious. For instance, should we put a catch around fork()
or around join()
? A ForkJoinTask
doesn't handle exception with catch, but instead requires us to check for isCompletedAbnormally
and then call getException
to get the exception thrown.
As CompletableFuture
allows chaining, it provides a cleaner way to pass exceptions from one call to the next. The terminal operation whenComplete
takes in a BiConsumer
as parameter -- the first argument to the BiConsumer
is the result from previous chain (or null
if exception thrown); the second argument is an exception (null if completes normally).
1 2 3 4 5 6 7 8 9 10 | CompletableFuture .completedFuture(Matrix.generate(nRows, nCols, rng::nextDouble)) .thenApply(m -> m.multiply(m)) .whenComplete((result, exception) -> { if (exception) { System.err.println(exception); } else { System.out.print(result); } } |
whenComplete
returns a CompletableFuture, surprisingly, despite it taking in a BiConsumer
-- in a sense, whenComplete
is more similar to peek
rather than forEach
.
handle
is similar to whenComplete
, but takes in a BiFunction
instead of a BiConsumer
, thus allowing the result or exception to be transformed.
Finally, exceptionally
handles exception by replacing a thrown exception with a value, similar to orElse
in Optional
.
1 2 3 4 | CompletableFuture .completedFuture(Matrix.generate(nRows, nCols, rng::nextDouble)) .thenApply(m -> m.multiply(m)) .exceptionally(ex -> Matrix.generate(nRows, nCols, () -> 0)); |
Promise
CompletableFuture
is similar to Promise
in other languages, notably JavaScript and C++ (std::promise
).
CompletionStage
In Java, CompletableFuture
also implements a CompletionStage
interface. Thus, you will find references to this interface in many places in the Java documentation. I find this name unintuitive and makes an already-confusing java documentation even harder to read.
Exercise
-
Change the following sequence of code so that
f()
,g()
andh()
are invoked asynchronously, usingCompletableFuture
.(i)
1 2 3
B b = f(a); C c = g(b); D d = h(c);
(ii)
1 2 3
B b = f(); C c = g(b); h(c); // no return value
(iii)
1 2 3 4
B b = f(a); C c = g(b); D d = h(b); E e = i(c, d);