Future
First of all, we have to observe that std::async
is a tool to execute a given task and return a std::future
object that holds the result of the computation once its available.
For example we can call result.get()
to block and wait for the result to arrive. Also, when the computation encountered an exception, it will be stored and rethrown to us as soon as we call result.get()
.
Java provides similar classes, the interface is Future
and the most relevant implementation is CompletableFuture
.
std::future#get
translates roughly to Future#get
. Even the exceptional behavior is very similar. While C++ rethrows the exception upon calling get
, Java will throw a ExecutionException
which has the original exception set as cause
.
How to obtain a Future?
In C++ you create your future object using std::async
. In Java you could use one of the many static helper methods in CompletableFuture
. In your case, the most relevant are
So in order to create a future that just prints Hello World!
, you could for example do
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> System.out.println("Hello World!"));
/*...*/
task.get();
Java not only has lambdas but also method references. Lets say you have a method that computes a heavy math task:
class MyMath {
static int compute() {
// Very heavy, duh
return (int) Math.pow(2, 5);
}
}
Then you could create a future that returns the result once its available as
CompletableFuture<Integer> task = CompletableFuture.runAsync(MyMath::compute);
/*...*/
Integer result = task.get();
async vs deferred
In C++, you have the option to specify a launch policy which dictates the threading behavior for the task. Let us put the memory promises C++ makes aside, because in Java you do not have that much control over memory.
The differences are that async
will immediately schedule creation of a thread and execute the task in that thread. The result will be available at some point and is computed while you can continue work in your main task. The exact details whether it is a new thread or a cached thread depend on the compiler and are not specified.
deferred
behaves completely different to that. Basically nothing happens when you call std::async
, no extra thread will be created and the task will not be computed yet. The result will not be made available in the meantime at all. However, as soon as you call get
, the task will be computed in your current thread and return a result. Basically as if you would have called the method directly yourself, without any async
utilities at all.
std::launch::async
in Java
That said, lets focus on how to translate this behavior to Java. Lets start with async
.
This is the simple one, as it is basically the default and intended behavior offered in CompletableFuture
. So you just do runAsync
or supplyAsync
, depending on whether your method returns a result or not. Let me show the previous examples again:
// without result
CompletableFuture<Void> task = CompletableFuture.runAsync(() -> System.out.println("Hello World!"));
/*...*/ // the task is computed in the meantime in a different thread
task.get();
// with result
CompletableFuture<Integer> task = CompletableFuture.supplyAsync(MyMath::compute);
/*...*/
Integer result = task.get();
Note that there are also overloads of the methods that except an Executor
which can be used if you have your own thread pool and want CompletableFuture
to use that instead of its own (see here for more details).
std::launch::deferred
in Java
I tried around a lot to mock this behavior with CompletableFuture
but it does not seem to be possibly without creating your own implementation (please correct me if I am wrong though). No matter what, it either executes directly upon creation or not at all.
So I would just propose to use the underlying task interface that you gave to CompletableFuture
, for example Runnable
or Supplier
, directly. In our case, we might also use IntSupplier
to avoid the autoboxing.
Here are the two code examples again, but this time with deferred behavior:
// without result
Runnable task = () -> System.out.println("Hello World!");
/*...*/ // the task is not computed in the meantime, no threads involved
task.run(); // the task is computed now
// with result
IntSupplier task = MyMath::compute;
/*...*/
int result = task.getAsInt();
Modern multithreading in Java
As a final note I would like to give you a better idea how multithreading is typically used in Java nowadays. The provided facilities are much richer than what C++ offers by default.
Ideally should design your system in a way that you do not have to care about such little threading details. You create an automatically managed dynamic thread pool using Executors
and then launch your initial task against that (or use the default executor service provided by CompletableFuture
). After that, you just setup an operation pipeline on the future object, similar to the Stream API and then just wait on the final future object.
For example, let us suppose you have a list of file names List<String> fileNames
and you want to
- read the file
- validate its content, skip it if its invalid
- compress the file
- upload the file to some web server
- check the response status code
and count how many where invalid, not successfull and successfull. Suppose you have some methods like
class FileUploader {
static byte[] readFile(String name) { /*...*/ }
static byte[] requireValid(byte[] content) throws IllegalStateException { /*...*/ }
static byte[] compressContent(byte[] content) { /*...*/ }
static int uploadContent(byte[] content) { /*...*/ }
}
then we can do so easily by
AtomicInteger successfull = new AtomicInteger();
AtomicInteger notSuccessfull = new AtomicInteger();
AtomicInteger invalid = new AtomicInteger();
// Setup the pipeline
List<CompletableFuture<Void>> tasks = fileNames.stream()
.map(name -> CompletableFuture
.completedFuture(name)
.thenApplyAsync(FileUploader::readFile)
.thenApplyAsync(FileUploader::requireValid)
.thenApplyAsync(FileUploader::compressContent)
.thenApplyAsync(FileUploader::uploadContent)
.handleAsync((statusCode, exception) -> {
AtomicInteger counter;
if (exception == null) {
counter = statusCode == 200 ? successfull : notSuccessfull;
} else {
counter = invalid;
}
counter.incrementAndGet();
})
).collect(Collectors.toList());
// Wait until all tasks are done
tasks.forEach(CompletableFuture::join);
// Print the results
System.out.printf("Successfull %d, not successfull %d, invalid %d%n", successfull.get(), notSuccessfull.get(), invalid.get());
The huge benefit of this is that it will reach max throughput and use all hardware capacity offered by your system. All tasks are executed completely dynamic and independent, managed by an automatic pool of threads. And you just wait until everything is done.
deferred
/async
launch policies might not be ubiquitous terminology that also exists in Java, which would require someone to be familiar with both languages. – Human-Compilerdeferred
just create aSupplier
orRunnable
and call it when you need it. Wrap a fakeCompletableFuture
around it if you want a future like C++ gives you. – ZabuzardScheduledExecutorService
and create a task pipeline on theCompletableFuture
objects, as in "do this, then that, when its done do that, afterwards this, ...". – Zabuzard