Kotlin协程vs CompletableFuture

时间:2019-10-20 21:00:06

标签: java kotlin concurrency coroutine kotlin-coroutines

有人可以解释为什么人们应该使用协程吗?是否有一些协程代码示例显示了比常规Java并发代码(没有神奇的delay()函数,没有人在生产中使用delay())的完成时间更短的示例?

在我的个人示例中,协程(第1行)与Java代码(第2行)相反。也许我做错了什么?

示例:

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

其他问题:

为什么我应该创建此协程包装器await()?似乎并不能改善代码的协程版本,否则get()方法会在inappropriate blocking method call上抱怨吗?

4 个答案:

答案 0 :(得分:5)

协程的目标不是“更好的完成时间”。坦白说,成功的目标是使协程更容易使用

也就是说,您在代码中所做的根本不是 比较两个替代方法速度的好方法。比较Java中事物的速度并获得逼真的结果极其困难,因此在尝试使用How do I write a correct micro-benchmark in Java?之前,应至少阅读它。您目前试图比较两段Java代码的方式将骗您关于代码的实际性能行为。

要回答您的其他问题,答案是您不应创建该await方法。无论协程代码在get()中,还是不使用java.util.concurrent.FuturesuspendCancellableCoroutine,都应将它们与协同程序代码一起使用。如果您想使用CompletableFuture,请使用the provided library从协程代码与之交互。

答案 1 :(得分:2)

这是我用于基准测试的代码的清理版本。注意,我从测量的代码中删除了print,因为打印本身是一项重量级的操作,涉及互斥锁,JNI,阻塞输出流等。相反,我更新了一个volatile变量。

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

我的结果是案例一号是6.5秒,而案例二是7秒。这是7%的差异,并且可能是针对此确切场景的特定情况,并不是您通常会认为这两种方法之间的差异。

在基于CompletionStage的程序中选择协程的原因绝对不是那7%,而是便利性的巨大差异。要了解我的意思,我邀请您通过调用main而不使用computeAsync来重写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()

答案 2 :(得分:0)

切换到此kotlinx-coroutines-jdk8库并将sleep(1)添加到我的getText()函数中

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)
        "."
    }
}

我使协程版本比Java版本快!!!显然,当有一些延迟时,这个额外的协程层变得合理。

答案 3 :(得分:0)

我的2个版本的compute方法,没有重写方法签名。我想我明白你的意思。使用协程,我们以熟悉的顺序样式编写复杂的并行代码。但是协程await包装器由于暂停技术而无法完成这项工作,只是实现了与我相同的逻辑。

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