我做了一些测试,比较了使用异步作为延迟结果的方法和CompletableDeferred与Job或startCoroutine的组合来完成相同工作的速度。 总之,有3个用例:
这是一个代码:
import com.source.log.log
import kotlinx.coroutines.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.startCoroutine
import kotlin.system.measureNanoTime
import kotlin.system.measureTimeMillis
/**
* @project Bricks
* @author SourceOne on 28.11.2019
*/
/*I know that there are better ways to benchmark speed
* but given the produced results this method is fine enough
* */
fun benchmark(warmUp: Int, repeat: Int, action: suspend () -> Unit): Pair<List<Long>, List<Long>> {
val warmUpResults = List(warmUp) {
measureNanoTime {
runBlocking {
action()
}
}
}
val benchmarkResults = List(repeat) {
measureNanoTime {
runBlocking {
action()
}
}
}
return warmUpResults to benchmarkResults
}
/* find way to cancel startedCoroutine when deferred is
* canceled (currently you have to cancel whole context)
* */
fun <T> CoroutineScope.completable(provider: suspend () -> T): Deferred<T> {
return CompletableDeferred<T>().also { completable ->
provider.startCoroutine(
Continuation(coroutineContext) { result ->
completable.completeWith(result)
}
)
}
}
suspend fun calculateAsyncStep() = coroutineScope {
val list = List(10000) {
async { "i'm a robot" }
}
awaitAll(*list.toTypedArray())
}
suspend fun calculateCDLStep() = coroutineScope {
val list = List(10000) {
CompletableDeferred<String>().also {
launch {
it.complete("i'm a robot")
}
}
}
awaitAll(*list.toTypedArray())
}
suspend fun calculateCCDLStep() = coroutineScope {
val list = List(10000) {
completable { "i'm a robot" }
}
awaitAll(*list.toTypedArray())
}
fun main() {
val labels = listOf("async", "cdl", "ccdl")
val collectedResults = listOf(
mutableListOf<Pair<List<Long>, List<Long>>>(),
mutableListOf(),
mutableListOf()
)
"stabilizing runs".log()
repeat(2) {
println("async $it")
benchmark(warmUp = 25, repeat = 200) {
calculateAsyncStep()
}
println("CDL $it")
benchmark(warmUp = 25, repeat = 200) {
calculateCDLStep()
}
println("CCDL $it")
benchmark(warmUp = 25, repeat = 200) {
calculateCCDLStep()
}
}
"\n#Benchmark start".log()
val benchmarkTime = measureTimeMillis {
repeat(100) {
println("async $it")
collectedResults[0] += benchmark(warmUp = 25, repeat = 200) {
calculateAsyncStep()
}
println("CDL $it")
collectedResults[1] += benchmark(warmUp = 25, repeat = 200) {
calculateCDLStep()
}
println("CCDL $it")
collectedResults[2] += benchmark(warmUp = 25, repeat = 200) {
calculateCCDLStep()
}
}
}
"\n#Benchmark completed in ${benchmarkTime}ms".log()
"#Benchmark results:".log()
val minMaxAvg = collectedResults.map { stageResults ->
stageResults.map { (_, benchmark) ->
arrayOf(
benchmark.minBy { it }!!, benchmark.maxBy { it }!!, benchmark.average().toLong()
)
}
}
minMaxAvg.forEachIndexed { index, list ->
"results for: ${labels[index]} [min, max, avg]".log()
list.forEach { results ->
"${results[0]}\t${results[1]}\t${results[2]}".log()
}
}
}
毫不奇怪,前两个用例(异步和cdl)彼此非常接近,并且异步总是更好(因为您没有创建工作来完成延迟对象的开销),而是比较异步与CompletableDeferred + startCoroutine之间有一个巨大的差距(几乎2倍),而最后一个则受青睐。为什么会有如此大的差异?如果有人知道,为什么我们不应该只使用CompletableDeferred + startCoroutine包装器(如此处的completable()
)而不是异步使用?
加法1: 这是1000分的示例: 异步和cdl结果中存在不断的尖峰,而ccdl中可能存在尖峰(也许是gc?),但ccdl却很少。我将以改变的测试顺序重新运行这些测试,但似乎与协程机制下的某些东西有关。
Edit1:
我已经接受了Marko Topolnik的回答,但是,如果您在启动范围内等待结果,您仍然可以使用这种“按他所说的”裸启动方法。
例如,如果您将启动几个有延迟的协程(异步),并且在该作用域的末尾,您将等待所有协程,那么ccdl方法将按预期工作(至少从我在测试中所见)。 / p>
答案 0 :(得分:0)
由于launch
和async
被构建为低级基元createCoroutineUnintercepted()
之上的一层,而startCoroutine
实际上是对其的直接调用,因此基准测试结果没有任何意外。
为什么我们不应该仅使用
CompletableDeferred
+startCoroutine
包装器(例如此处的completable()
)而不是async
?
代码中的注释已提示答案:
/*
* find way to cancel startedCoroutine when deferred is
* canceled (currently you have to cancel whole context)
*/
与startCoroutine
短路的层正是处理取消,协程层次结构,异常处理和传播等内容的层。
这是一个简单的示例,向您展示了用裸露的协程替换launch
时发生的故障之一:
fun main() = runBlocking {
bareLaunch {
try {
delay(1000)
println("Coroutine done")
} catch (e: CancellationException) {
println("Coroutine cancelled, the exception is: $e")
}
}
delay(10)
}
fun CoroutineScope.bareLaunch(block: suspend () -> Unit) =
block.startCoroutine(Continuation(coroutineContext) { Unit })
fun <T> CoroutineScope.bareAsync(block: suspend () -> T) =
CompletableDeferred<T>().also { deferred ->
block.startCoroutine(Continuation(coroutineContext) { result ->
result.exceptionOrNull()?.also {
deferred.completeExceptionally(it)
} ?: run {
deferred.complete(result.getOrThrow())
}
})
}
运行此命令时,您会看到10毫秒后取消了协程。 runBlocking
生成器没有意识到它必须等待它完成。如果将bareLaunch {
替换为launch {
,则将恢复子协程正常完成的设计行为。 bareAsync
也会发生同样的事情。