Sometimes, what you need is not only execute a task in an executor but also return the result of this task to the calling code. It is possible but inconvenient to do it with Runnables. In order to simplify the task, we can use Callable and Future.
The Callable interface
The executor supports a class of tasks named Callable, which returns the result and may throw an exception. This functional interface belongs to the java.util.concurrent package. Kotlin automatically converts functional interfaces to lambdas, which makes your code more concise and readable. Let's take a look at an example.
Here is a Callable that emulates a long-running task and returns a number that was "calculated".
import java.util.concurrent.*
val generator = Callable {
TimeUnit.SECONDS.sleep(5)
70000
}The result of generator can be obtained by the call() method:
println(generator.call()) // after 5 seconds, 70000 will be printedSubmitting a Callable and obtaining a Future
When we submit a Callable to the executor service, it cannot return the result directly, since the submit() method does not wait until the task completes. Instead, the executor returns a special object called Future, which wraps the actual result that may not even exist yet. This object represents the result of an asynchronous computation (task).
import java.util.concurrent.*
val executor: ExecutorService = Executors.newSingleThreadExecutor()
val future = executor.submit(
Callable {
TimeUnit.SECONDS.sleep(5)
70000
}
)Until the task completes, the actual result is not present in the future. To check that, we can use the isDone property. Most likely, it will return false if you get it immediately after obtaining a new future.
println(future.isDone) // most likely it is falseGetting the actual result of a task
We can retrieve the result from a future only by using the get() method.
val result = future.get()It returns the result when the computation has completed; in other words, it blocks the current thread and waits for the result. This method may throw two exceptions: ExecutionException and InterruptedException, which we omit here for brevity.
If a submitted task executes an infinite loop or waits for an external resource for too long, the thread that invokes get() will be blocked all that time. To prevent that, we can use an overloaded version of get() with a waiting timeout.
val result = future.get(10, TimeUnit.SECONDS) // it blocks the current thread In this case, the calling thread waits for the computation to complete for 10 seconds at most. If the timeout ends, the method throws TimeoutException.
Canceling a task
The Future class provides an instance method named cancel(), which attempts to cancel the execution of a task. This method is more complex than it might seem at first glance.
An attempt to cancel will fail if the task has already completed, has already been canceled, or could not be canceled for some other reason. If successful or if this task has not yet started when the method is invoked, the task will not be executed.
The method takes a boolean parameter, which determines whether the thread executing this task should be interrupted in an attempt to stop the task (in other words, whether to stop the already running task or not).
future1.cancel(true) // try to cancel even if the task is executing now
future2.cancel(false) // try to cancel only if the task is not executingSince passing true involves interruptions, the cancellation of an executing task is guaranteed only if it handles InterruptedException correctly and checks the flag Thread.currentThread().isInterrupted.
If we invoke future.get() on a successfully canceled task, the method throws a CancellationException. If you do not want to deal with it, you may check whether a task was canceled by the isCancelled property.
The advantage of using Callable and Future
The approach we are learning here allows us to do something useful during the time between obtaining a Future and getting the actual result. In this time interval, we can submit several tasks to the executor and only after that wait for all the results to be aggregated.
import java.util.concurrent.*
val executor = Executors.newFixedThreadPool(4)
val future1 = executor.submit(
Callable {
TimeUnit.SECONDS.sleep(5)
700000
}
)
val future2 = executor.submit(
Callable {
TimeUnit.SECONDS.sleep(5)
900000
}
)
val result = future1.get() + future2.get() // waiting for both results
println(result) // 1600000If you have a modern computer, these tasks may be executed in parallel.
Methods invokeAll and invokeAny
In addition to the features described above, there are two useful methods for submitting batches of Callable to an executor:
invokeAll()accepts a prepared collection of callables and returns a collection of futures;invokeAny()also accepts a collection of callables and returns the result (not a future!) of the one that has completed successfully.
Both methods also have overloaded versions that accept execution timeout, which is often needed in real life.
Suppose that we need to calculate several numbers in separate tasks and then to sum up the numbers in the main thread. It is easy to do using the invokeAll() method.
import java.util.concurrent.*
val executor = Executors.newFixedThreadPool(4)
// three "difficult" tasks
val callables = listOf(
Callable { 1000 },
Callable { 2000 },
Callable { 1500 }
)
val futures = executor.invokeAll(callables)
var sum = 0
for (future in futures) sum += future.get() // blocks on each future to get the result
println(sum)Summary
Let's summarize what we've learned about Callable and Future.
To get the result of an asynchronous task executed in ExecutorService, you need to take three steps:
create an object representing a
Callabletask;submit the task to
ExecutorServiceand obtain aFuture;invoke
get()to receive the actual result when you need it.
Using Future allows us not to block the current thread until we do want to receive the result of a task. It is also possible to start multiple tasks and then get all the results in order to aggregate them in the current thread. In addition to making your program more responsive, it will speed up your calculations if your computer supports parallel execution of threads.
You may also use the cancel() method, the isDone property, and the isCancelled property of a future, but be careful with exception handling when using them. Unfortunately, we cannot give you all possible recipes and best practices within the lesson, but the skills will come with experience. The crucial thing, especially in multi-threaded programming, is to carefully read the documentation.