Java Futures and Kotlin Co-Routines — a comparison
Background
Asynchronous programming
Asynchronous programming helps us perform many tasks in parallel. Do read my other posts on the subject:
Java Asynchronous programming
Asynchronous programming in Java has come a long way. Java futures have been constantly improving and are a mainstay of many code bases. Java streams have Java Futures as a base.
Kotlin Asynchronous programming
Kotlin is a new language built by the folks from Idea. Android has accepted it open arms. Here is a link to help us understand “Why ?”
Kotlin has an aspect called coroutines which helps with asynchronous programming.
Examples
In this post, we will code some examples of Java futures and Kotlin coroutines.
Java Futures
Java futures are built around the future interface. They help us look at aspects of asynchronous programming in Java.
We will now code a series of classes.
WorkManager
package future.tryout;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class WorkManager {
private ExecutorService es = Executors.newCachedThreadPool();
CompletionStage<Result> workForTime(int millis) {
CompletableFuture<Result> future = new CompletableFuture<>();
es.submit(()->{
try {
Thread.sleep(millis);
future.complete(Result.of(millis, true, "Worked for " + millis + " milliseconds"));
} catch (InterruptedException e) {
future.complete(Result.of(millis, false, "Error : " + e.getMessage()));
}
});
return future;
}
}
Workmanager creates a dummy worker which mimics a real work task by sleeping for a given number of milliseconds before a work task is complete.
It then returns a “Result” object. This work is wrapped in a “CompletionStage”. This should help us chain disparate tasks.
Note: As per the Java API, when we sleep, we must also catch or throw an “Interruptedexception”.
Result
package future.tryout;
import lombok.Value;
@Value(staticConstructor = "of")
class Result {
int time;
boolean success;
String message;
}
A simple data class with three fields
- int time — the time in milliseconds we waited
- boolean success — was the work a success or not
- String message — the message from the work task
Using Lombok here to reduce java boilerplate code.
FutureExamples
package future.tryout;
import java.util.Optional;
import java.util.concurrent.CompletionStage;
public class FutureExamples {
private WorkManager manager = new WorkManager();
The main examples class.
We create the workmanager instance and we should be good to go.
Note:
- By no means is this an exhaustive list of all the methods we could use with Java Futures. Please see the Java API and work out what you need
- These are only examples of a possible way to use these Futures methods.
thenRun
public CompletionStage<Void> workWithRun(int time1, int time2) {
return
manager
.workForTime(time1)
.thenRun(() -> {
manager.workForTime(time2);
});
}
The first example is thenRun.
We have two parameters here, time1 and time2. Once we finish time1 work, we want to run time2.
Notice here thenRun returns a CompletionStage with Void. It does not return our “Result” object. This may be fine for some use cases.
Let’s see if we can return a result in the next method.
thenApply
public CompletionStage<CompletionStage<Result>> workWithApply(int time1, int time2) {
return
manager
.workForTime(time1)
.thenApply(
result -> manager.workForTime(time2) // If we wanted we could do something with result here
);
}
This is a thenApply Example
If we also want to return the result of time2 task, we could use thenApply. As seen above we get the return with is a has the second task result wrapped in two completion stages.
Lets see in the next example if we can improve on this.
thenCompose
public CompletionStage<Result> workWithCompose(int time1, int time2) {
return
manager
.workForTime(time1)
.thenCompose(
result -> manager.workForTime(time2) // If we wanted we could do something with result here
);
}
This is a thenCompose Example
As seen above we get the return with is a has the result in one completion stage.
If you have used the streams api, this is analogous to a flatMap
thenAcceptBoth
public CompletionStage<Void> workWithAccept(int time1, int time2, int time3) {
return
manager
.workForTime(time1)
.thenAcceptBoth(
manager.workForTime(time2), (result, result2) -> {
if (result.isSuccess() && result2.isSuccess()) {
manager.workForTime(time3);
}
});
}
This is an example of thenAcceptBoth with three tasks
Here we execute three tasks, but as before we still just get a void CompletionStage. Lets see if we can improve on that and get a result.
thenCombine
public CompletionStage<Optional<CompletionStage<Result>>> workWithCombine(int time1, int time2, int time3) {
return
manager
.workForTime(time1)
.thenCombine(manager.workForTime(time2),(result, result2) -> {
if (result.isSuccess() && result2.isSuccess()) {
return Optional.of(manager.workForTime(time3));
}
return Optional.empty();
});
}
This is an example of thenCombine with three tasks
Here we execute three tasks and get a Result of the final task.
Exceptions
What if we wanted to know if there was an exception and then we wanted to act on the exception on any CompletionStage ?
We could use “whenComplete” on the result CompletionStage. This is true of all the above examples.
(void, exception) -> {
if (exception == null) {
// triggering stage completed normally
} else {
// triggering stage completed exceptionally
}
}
Kotlin Coroutines
Kotlin coroutines help us with aspects of asynchronous programming in Kotlin. Do go through the guide here
We will now code a series of classes.
WorkManager
package coroutine.tryout
import kotlinx.coroutines.delay
internal class WorkManager {
suspend fun workForTime(millis: Int): Result {
delay(millis.toLong())
return Result(millis, true, "Worked for $millis milliseconds")
}
}
Workmanager creates a dummy worker which mimics a real work task by sleeping for a given number of milliseconds before a work task is complete.
deplay is a special suspending function that does not block a thread, but suspends coroutine and it can be only used from a coroutine.
Result
package coroutine.tryout
data class Result(val time: Int, val isSuccess: Boolean, val message: String)
A simple data class with three fields
- int time — the time in milliseconds we waited
- boolean success — was the work a success or not
- String message — the message from the work task
Love Data classes!
Note: Java may soon have a similar concept i.e. Record classes
CoroutineExamples
package coroutine.tryout
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
class CoroutineExamples {
private val manager = WorkManager()
The main examples class.
We create the workmanager instance and we should be good to go.
Note:
- By no means is this an exhaustive list of all the functions, we could use with Kotlin CoRoutines. Please see the CoRoutines API and work out what you need
- These are only examples of a possible way to use these CoRoutine methods.
Synchronous run with two functions
suspend fun workWithTwoTimes(time1: Int, time2: Int): Pair<Result, Result> {
return coroutineScope {
val result1 = manager.workForTime(time1) // work1
val result2 = manager.workForTime(time2) // work2 after work1
Pair(result1, result2)
}
}
Here we perform two tasks in a coroutine and return the result of both the tasks in a pair class.
Parallel run with two functions
suspend fun workWithTwoTimesParallel(time1: Int, time2: Int): Pair<Result, Result> {
return coroutineScope {
val work1 = async { manager.workForTime(time1) } // Async work1
val result2 = manager.workForTime(time2) // work2 while work1 is working
val result1 = work1.await() // non-blocking wait
Pair(result1, result2)
}
}
Here we perform two tasks, both in parallel and then return the result of both the tasks in a Pair class. Not much more to it. Sweet!
Synchronous run with three functions
suspend fun workWithThreeTimes(time1: Int, time2: Int, time3: Int): Triple<Result, Result, Result> {
return coroutineScope {
val result1 = manager.workForTime(time1) // work1
val result2 = manager.workForTime(time2) // work2 after work1
val result3 = manager.workForTime(time3) // work3 after work2
Triple(result1, result2, result3)
}
}
Here we perform three tasks in a coroutine and return the three results of the tasks in a Triple class.
Parallel run with three functions
suspend fun workWithThreeTimesParallel(time1: Int, time2: Int, time3: Int): Triple<Result, Result, Result> {
return coroutineScope {
val work1 = async { manager.workForTime(time1) } // Async work1
val work2 = async { manager.workForTime(time2) } // Async work2 while work1 is working
val result3 = manager.workForTime(time3) // work3 while work1 and work2 are working
val result1 = work1.await() // non-blocking wait
val result2 = work2.await()// non-blocking wait
Triple(result1, result2, result3)
}
}
Here we perform three works, in parallel, in a coroutine and return the three results of the tasks in a Triple class.
Note:
- “async/await” in Java i.e “wait/lock/notify/join” are thread blocking paradigms in java involving thread synchronization.
- “async/await” in kotlin is explained as : “Awaits for completion of this value without blocking a thread”
- We could use launch in place of async if we do not care about getting the result
- Could we do something like this in Java i.e. perform a task and when the task is complete get a notification without blocking a thread ? Yes, we could choose to use any event/message based framework like Vert.x, Akka Actors, Play, etc…
Test
package coroutine.tryout
import io.kotlintest.specs.StringSpec
import kotlin.system.measureTimeMillis
class CoroutineExamplesTimeTest : StringSpec({
val coroutineExamples = CoroutineExamples()
val time1 = 300
val time2 = 200
val time3 = 100
"workWithTwoTimes should return result time1 and result time2" {
val time = measureTimeMillis {
coroutineExamples.workWithTwoTimes(time1, time2)
}
println("workWithTwoTimes in $time ms")
}
"workWithThreeTimes should return result time1, time2 and time3" {
val time = measureTimeMillis {
coroutineExamples.workWithThreeTimes(time1, time2, time3)
}
println("workWithThreeTimes in $time ms")
}
"workWithTwoTimesParallel should return result time1 and result time2" {
val time = measureTimeMillis {
coroutineExamples.workWithTwoTimesParallel(time1, time2)
}
println("workWithTwoTimesParallel in $time ms")
}
"workWithThreeTimesParallel should return result time1, time2 and time3" {
val time = measureTimeMillis {
coroutineExamples.workWithThreeTimesParallel(time1, time2, time3)
}
println("workWithThreeTimesParallel in $time ms")
}
})
As seen above we will use measureTimeMillis as a block to test our suspend task functions.
The test above should be self explanatory.
Test output
workWithTwoTimes in 509 ms
workWithThreeTimes in 612 ms
workWithTwoTimesParallel in 304 ms
workWithThreeTimesParallel in 307 ms
We get the result as above.
Running two tasks we use 509ms and running two tasks in parallel we use 304 ms. Makes sense.
Running three tasks we use 612ms and running three tasks in parallel we use 307 ms. Again Makes sense.
Verdict
Kotlin Coroutines make a lot of sense. They seem simple to work with and thus development should be simpler.
What do you think ? Feel free to add any comments/advice/corrections in the comments section below.
Thanks for your time.
Code
All code is checked in here