简体   繁体   中英

Kotlin Coroutines vs CompletableFuture

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 ?

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.

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.

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()

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!!
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM