关于kotlin中的async/await
大家肯定听说了它们可以并行执行,达到一个缩短程序执行耗时的效果。
大家最常看到的例子基本是这样的:
/**
* 代码1
*/
suspend fun intValue1(): Int {
delay(2000)
return 15
}
suspend fun intValue2(): Int {
delay(1000)
return 20
}
runBlocking {
val elapsedTime = measureTimeMillis {
val value1 = async { intValue1() }
val value2 = async { intValue2() }
val result1 = value1.await()
val result2 = value2.await()
// 输出:15 + 20 = 35
println("$result1 + $result2 = ${result1 + result2}")
}
// 输出:total time: 2013
println("total time: $elapsedTime")
}
这样子是并行执行两个不同的耗时方法。那如果我们是要并行执行同一个耗时的方法呢?用一个for循环解决
/**
* 代码2
*/
suspend fun intValue1(): Int {
delay(2000)
return 15
}
runBlocking {
val elapsedTime = measureTimeMillis {
val value1 = async { intValue1() }
var result = 0
for (i in 0 until 5) {
result += value1.await()
}
// 输出:result = 75
println("result = $result")
}
// 输出:total time: 2002
println("total time: $elapsedTime")
}
那如果要传参数呢?
/**
* 代码3
*/
suspend fun intValue1(i: Int): Int {
delay(2000)
return i
}
runBlocking {
val elapsedTime = measureTimeMillis {
var result = 0
for (i in 0 until 5) {
// 将之前的val value1 = async { intValue1() } 放到for循环中
// 并且传入参数
result += async { intValue1(i) }.await()
}
// 输出:result = 10 (0 + 1 + 2 + 3 + 4 = 10)
println("result = $result")
}
// 输出:total time: 10023(2000 * 5 = 10000)
println("total time: $elapsedTime")
}
诶?结果是对的,但是时间变成串行执行的时间了,而且看到async
划线提示:Redundant 'async' call may be reduced to 'kotlinx.coroutines.withContext'(提示我们用withContext
替代,withContext
就是一个会阻塞当前协程的一个方法。)
然后尝试仿造代码3,将async
和await
拆开来写:
/**
* 代码4
*/
suspend fun intValue1(i: Int): Int {
delay(2000)
println("intValue1")
return i
}
runBlocking {
val elapsedTime = measureTimeMillis {
val value1 = async { intValue1(0) }
val value2 = async { intValue1(1) }
val value3 = async { intValue1(2) }
val value4 = async { intValue1(3) }
val value5 = async { intValue1(4) }
val result = value1.await() + value2.await() + value3.await() +
value4.await() + value5.await()
// 输出:result = 10 (0 + 1 + 2 + 3 + 4 = 10)
println("result = $result")
}
// 输出:total time: 2016
println("total time: $elapsedTime")
}
时间又变成了并行执行
这中间的差异和问题是什么呢?本着好奇的心态,开始研究kotlin关于await/async
的源码,和编辑的中间Java产物,但是因为水平有限,找不到可以解释的实现原理。最后只能通过各种println
研究耗时,总算发现了一些东西。
先上结论:
1.async
会准备启动一个协程,但是不会立即执行,需要等待start
或者await
的命令来启动
2.await
和start
会把之前启动的async
协程全部启动
3.await
不会阻塞线程,但是它会通过一个while(true)
等待协程运行的结果
感谢@寻水的鱼Chock的指正,之前的结论有误。
1.async
如果没有指定启动模式,就会直接启动一个协程,并直接执行。如果指定了CoroutineStart.LAZY
就启动一个协程并挂起,不会立即执行,需要等待start
或者await
的命令来启动
2.调用await
方法会去查询对应的async
是否有结果,如果没有结果,则通过while(true)
进行等待,直到有结果返回
- 用以上结论可以解释代码3,为什么在一个for循环中,
result += async { intValue1(i) }.await()
会变成一个串行的过程,就是因为async
启动了一个协程,然后又立马去await
执行它,这时候一个while(true)
循环等待async
运行的结果,获取到结果后,才又进入下一个for循环。所以本来以为的for循环并行,变成了for循环串行。 解释代码4,前面通过async
准备了5个协程,然后第一个value1.await()
启动了前面5个协程,而后面的几个await
只是去取结果,所以就变成了一个并行的过程。- 解释代码4,前面通过
async
准备了5个协程并开始执行,而第一个value1.await()
会通过while(true)
等待async
的执行结果,当value1.await()
获取到结果后,后面几个await
也陆续获取到了结果,所以就变成了一个并行的过程。 - 对于代码2,第一个for循环的
await
是去启动并等待执行结果,而后续的几个await
只是去取同一个协程运行的结果而已。
结论的证明:
/**
* 结论1和结论2的证明
*/
suspend fun intValue1(i: Int): Int {
delay(1000)
println("我是intValue$i")
return i
}
runBlocking {
val elapsedTime = measureTimeMillis {
var start = System.currentTimeMillis()
val value1 = async { intValue1(1) }.await()
println("value1执行时间:time = ${System.currentTimeMillis() - start}")
// 输出:
//我是intValue1
//value1执行时间:time = 1014
//因为调用了await
start = System.currentTimeMillis()
val value2 = async { intValue1(2) }
println("value2执行时间:time = ${System.currentTimeMillis() - start}")
// 输出:
//value2执行时间:time = 1
//因为只是准备协程,并没有启动
val value3 = async { intValue1(3) }
println("value3执行时间:time = ${System.currentTimeMillis() - start}")
// 输出:
//value3执行时间:time = 1
//因为只是准备协程,并没有启动
val value4 = async { intValue1(4) }.await()
println("value4执行时间:time = ${System.currentTimeMillis() - start}")
// 输出:
//我是intValue2
//我是intValue3
//我是intValue4
//value4执行时间:time = 1003
//同时并行启动了前面的几个协程
start = System.currentTimeMillis()
println("我来取value2, value3的结果:value2 = ${value2.await()}, value3 = ${value3.await()}")
println("取结果的时间:time = ${System.currentTimeMillis() - start}")
// 输出:
//我来取value2, value3的结果:value2 = 2, value3 = 3
//取结果的时间:time = 0
//因为协程已经启动过了,已经有结果,不需要再次启动协程
}
println("total time: $elapsedTime")
// 输出:total time: 2017
//因为只执行过两次耗时的串行操作,所以是2000ms
}
// 总的输出结果:
我是intValue1
value1执行时间:time = 1014
value2执行时间:time = 1
value3执行时间:time = 1
我是intValue2
我是intValue3
我是intValue4
value4执行时间:time = 1003
我来取value2, value3的结果:value2 = 2, value3 = 3
取结果的时间:time = 0
total time: 2017
/**
* 结论3
* await的源码
*/
override suspend fun await(): T = awaitInternal() as T
internal suspend fun awaitInternal(): Any? {
// fast-path -- check state (avoid extra object creation)
while (true) { // lock-free loop on state
val state = this.state
if (state !is Incomplete) {
// already complete -- just return result
if (state is CompletedExceptionally) { // Slow path to recover stacktrace
recoverAndThrow(state.cause)
}
return state.unboxState()
}
if (startInternal(state) >= 0) break // break unless needs to retry
}
return awaitSuspend() // slow-path
}
最后
回到最开始的需求,如果要循环并行执行耗时操作,要怎么做呢?我能想到的方式如下,如果大佬们有更好的建议,欢迎评论:
suspend fun intValue1(i: Int): Int {
delay(1000)
println("我是intValue$i")
return i
}
runBlocking {
val elapsedTime = measureTimeMillis {
val asyncList = mutableListOf<Deferred<Int>>()
for (i in 0 until 5) {
asyncList.add(async { intValue1(i) })
}
var result = 0
for (item in asyncList) {
result += item.await()
}
// result = 10
println("result = $result")
}
// total time: 1014
println("total time: $elapsedTime")
}