5

Can anybody explain me why people should use coroutines? Is there some coroutine code example which shows better completion time against regular java concurrent code (without magical delay() function, nobody uses delay() in production) ?

In my personal example coroutines(line 1) are suck against java code(line 2). Maybe i did something wrong?

Example:

import kotlinx.coroutines.*
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future

@ExperimentalCoroutinesApi
fun main() = runBlocking {
    val begin = Instant.now().toEpochMilli()
    val jobs = List(150_000) {
        GlobalScope.launch { print(getText().await()) } // :1
//        CompletableFuture.supplyAsync { "." }.thenAccept { print(it) } // :2
    }
    jobs.forEach { it.join() }
    println(Instant.now().toEpochMilli() - begin)
}

fun getText(): Future<String> {
    return CompletableFuture.supplyAsync {
        "."
    }
}

@ExperimentalCoroutinesApi
suspend fun <T> Future<T>.await(): T = suspendCancellableCoroutine { cont ->
    cont.resume(this.get()) {
        this.cancel(true)
    }
}

Additional question:

Why i should create this coroutine wrapper await()? It seems does not improve coroutine version of code otherwise get() method complains on inappropriate blocking method call?

lalilulelo_1986
  • 526
  • 3
  • 7
  • 18
  • 5
    `delay()` is actually an excellent approximation of production code because a coroutine gets suspended while doing nothing but wait for the response to arrive. Here's a nice summary: use threads to parallelize work, use coroutines to parallelize waiting. – Marko Topolnik Oct 21 '19 at 07:25

4 Answers4

12

The goal of coroutines is not "better completion time." The goal -- at which it succeeds quite well, honestly -- is that coroutines are easier to use.

That said, what you've done in your code is not at all a good way to compare the speed of two alternate approaches. Comparing the speed of things in Java and getting realistic results is extremely hard, and you should read How do I write a correct micro-benchmark in Java? at a minimum before attempting it. The way you are currently attempting to compare two pieces of Java code will lie to you about the realistic performance behavior of your code.

To answer your additional question, the answer is that you should not create that await method. You should not use get() -- or java.util.concurrent.Future -- with coroutine code, whether it's in suspendCancellableCoroutine or otherwise. If you want to use a CompletableFuture, use the provided library to interact with it from coroutine code.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • Thank you for your answer. I added this library in my sample and got much better result, but still worse than Java. Now i've understood purpose of this wrappers. My wrapper was not really suspendable. – lalilulelo_1986 Oct 20 '19 at 22:26
3

Here's a cleaned-up version of your code that I used for benchmarking. Note I removed print from the measured code because printing itself is a heavyweight operation, involving mutexes, JNI, blocking output streams, etc. Instead I update a volatile variable.

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.lang.Thread.sleep
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.TimeUnit.NANOSECONDS

@Volatile
var total = 0

@ExperimentalCoroutinesApi
fun main() = runBlocking {
    println("Warmup")
    measure(20_000)
    println("Measure")
    val begin = System.nanoTime()
    measure(40_000)
    println("Completed in ${NANOSECONDS.toMillis(System.nanoTime() - begin)} ms")
}

fun getText(): CompletableFuture<Int> {
    return CompletableFuture.supplyAsync {
        sleep(1)
        1
    }
}

suspend fun measure(count: Int) {
    val jobs = List(count) {
        GlobalScope.launch { total += getText().await() } // :1
//        getText().thenAccept { total += it } // :2
    }
    jobs.forEach { it.join() }
}

My result is 6.5 seconds for case number one vs. 7 seconds for case number two. That's a 7% difference and it's probably very specific to this exact scenario, not something you'll generally see as a difference between the two approaches.

The reason to choose coroutines over CompletionStage-based programming is definitely not about those 7%, but about the massive difference in convenience. To see what I mean, I invite you to rewrite the main function by calling just computeAsync, without using future.await():

suspend fun main() {
    try {
        if (compute(1) == 2) {
            println(compute(4))
        } else {
            println(compute(7))
        }
    } catch (e: RuntimeException) {
        println("Got an error")
        println(compute(8))
    }
}

fun main_no_coroutines() {
    // Let's see how it might look!
}

fun computeAsync(input: Int): CompletableFuture<Int> {
    return CompletableFuture.supplyAsync {
        sleep(1)
        if (input == 7) {
            throw RuntimeException("Input was 7")
        }
        input % 3
    }
}

suspend fun compute(input: Int) = computeAsync(input).await()
Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
2

After switching to this kotlinx-coroutines-jdk8 library and adding sleep(1) to my getText() function

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.lang.Thread.sleep
import java.time.Instant
import java.util.concurrent.CompletableFuture

fun main() = runBlocking {
    val begin = Instant.now().toEpochMilli()
    val jobs = List(150_000) {
        GlobalScope.launch { print(getText().await()) } // :1
//        getText().thenAccept { print(it) } // :2
    }
    jobs.forEach { it.join() }
    println(Instant.now().toEpochMilli() - begin)
}

fun getText(): CompletableFuture<String> {
    return CompletableFuture.supplyAsync {
        sleep(1)
        "."
    }
}

i made coroutine version faster than java version!!! Apparently this additional coroutine layer becomes justified when there is some delay.

lalilulelo_1986
  • 526
  • 3
  • 7
  • 18
  • If you intend to block the thread like `sleep` does, then don't look into coroutines. They won't provide you much value over `Future.get()`. – Marko Topolnik Oct 21 '19 at 12:07
  • But in my example I've got much better result than with CompletableFuture. It seems sleep works good with coroutines. Looks like java does not block it. [kotlinx-coroutines-jdk8 library](https://github.com/Kotlin/kotlinx.coroutines/blob/master/integration/kotlinx-coroutines-jdk8/test/examples/simple-example-1.kt) has similar example – lalilulelo_1986 Oct 21 '19 at 14:09
  • I really wonder what you actually want to achieve. I also wonder what exactly code you are using that gives a much better result. In my experience, that `sleep(1)` is the dominant bottleneck and the rest doesn't matter. Furthermore, `CompletableFuture.await()` is nothing but a thin wrapper on the underlying Java mechanism, so it can hardly become faster than it. – Marko Topolnik Oct 21 '19 at 15:55
  • Just want to figure out how this coroutines work. I added code which i'm testing. – lalilulelo_1986 Oct 21 '19 at 16:53
  • I'm testing line 1 against line 2. – lalilulelo_1986 Oct 21 '19 at 17:06
  • But you also made some changes, so I don't know what exactly you're measuring. Also, you print from each job, but printing itself is a heavyweight operation that offsets the result. I made a benchmark based on your code and got pretty similar results, within 7% of each other. – Marko Topolnik Oct 21 '19 at 17:33
0

My 2 versions of compute method without rewriting methods signature. I think i've got your point. With coroutines we write complex parallel code in familiar sequential style. But coroutine await wrapper does not make this work due suspending technic, it just implement same logic that i did.

import java.lang.Thread.sleep
import java.util.concurrent.CompletableFuture

fun main() {
    try {
        if (compute(1) == 2) {
            println(compute(4))
        } else {
            println(compute(7))
        }
    } catch (e: RuntimeException) {
        println("Got an error")
        println(compute(8))
    }
}

fun compute(input: Int): Int {
    var exception: Throwable? = null
    val supplyAsync = CompletableFuture.supplyAsync {
        sleep(1)
        if (input == 7) {
            throw RuntimeException("Input was 7")
        }
        input % 3
    }.exceptionally {
        exception = it
        throw it
    }
    while (supplyAsync.isDone.not()) {}
    return if (supplyAsync.isCompletedExceptionally) {
        throw exception!!
    } else supplyAsync.get()
}

fun compute2(input: Int): Int {
    try {
        return CompletableFuture.supplyAsync {
            sleep(1)
            if (input == 7) {
                throw RuntimeException("Input was 7")
            }
            input % 3
        }.get()
    } catch (ex: Exception) {
        throw ex.cause!!
    }
}
lalilulelo_1986
  • 526
  • 3
  • 7
  • 18
  • The idea was to remove `.await()` from `compute` so it returns a `CompletableFuture` and then try to rewrite the caller-side logic to have the same effect as mine. This moves a lot of logic into `compute()` which wasn't there originally. – Marko Topolnik Oct 22 '19 at 08:28
  • And I don't understand your comment that it's not `.await()` that makes this work. It is precisely that method that converts your `CompletableFuture`-returning function into a result-returning function, allowing you to use the plain sequential idiom, including a big try-catch block that catches exceptions from any async invocation. – Marko Topolnik Oct 22 '19 at 08:31
  • I wanted to say that this is not coroutine's merit to make handle exceptions easier. – lalilulelo_1986 Oct 25 '19 at 18:36
  • Yes, that's how I understood you, but it doesn't make sense to me since it's just the coroutines and nothing else that make it so much easier. – Marko Topolnik Oct 25 '19 at 18:54