介绍
主要是学习如何在协程之中使用async函数,将函数进行挂起操作。
那么什么时候挂起函数?
例如网络请求函数,在发起网络请求后,等待后台接口数据返回的过程中,就属于挂起状态。也就是挂起函数。
函数执行过程中出现了等待那么这个函数可以说是挂起函数了。
参考资料来自:组合挂起函数 - Kotlin 语言中文站 (kotlincn.net)
默认顺序调用
例如有两个函数执行了一个远程服务,或者长时间计算的函数。需要延迟1秒钟之后才能够返回结果。
package com.zinyan.general
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了一些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}
/**
* 开始逻辑
*/
suspend fun main() {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("得到两个值的结果是 ${one + two}")
}
println("两个函数调用后的消耗时间为: $time 毫秒 ")
}
//输出
得到两个值的结果是 42
两个函数调用后的消耗时间为: 2050 毫秒
最后根据你的电脑的性能,你每次运行的时间结果可能有高有低,但是都将大于2000毫秒。
因为这2000毫秒是中间计算之后的等待时间。
并发 async
上面的例子中是两个函数互相依赖了。我们让它们进行默认顺序执行计算了,但是如果我们需要速度更快点呢?不想等待2秒这么长呢?
那么就需要两个函数进行并行了,而这种并行计算就是:并发。我们可以通过async来帮助我们实现。
async
很类似Launch
。它将会启动一个单独的协程,这个协程与其他的所有协程一起并发的工作,不同于Launch
的返回值是job
并且不附带任何结果值。
async
将会返回一个 Deferred
:一个轻量级的非阻塞Future
,一个将会在稍后提供结果的承诺。
可以通过.await()
得到这个延期之后的结果值。这个Deferred
其实就是一种变种后的job
。我们如果想中途取消,也是可以直接取消掉的。
通过示例我们来了解效果:
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了一些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}
/**
* 开始逻辑
*/
fun main() = runBlocking {
val time = measureTimeMillis {
val one = async{
doSomethingUsefulOne()
}
val two = async{
doSomethingUsefulTwo()
}
println("得到两个值的结果是 ${one.await() + two.await()}")
}
println("两个函数调用后的消耗时间为: $time 毫秒 ")
}
//输出
得到两个值的结果是 42
两个函数调用后的消耗时间为: 1030 毫秒
相较于第一种默认执行,我们使用并发之后,时间节省了1000毫秒。
注意: 使用协程进行并发操作,必须是显式的。也就是说需要我们开发主动调用。编译器不会隐式的给我们开启。
惰性启动 async
我们还可以在并发的时候进行惰性启动,也就是延迟启动并发。通过设置start =CoroutineStart.LAZY
。
在这种模式下,只有当我们调用await
获取协程数据的时候,才会启动async 的协程计算。
我们如果还想主动去启动并发的话,那么就需要通过调用start
函数来启动。
第一种:
/**
* 开始逻辑
*/
fun main() = runBlocking {
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY){
doSomethingUsefulOne()
}
val two = async(start = CoroutineStart.LAZY){
doSomethingUsefulTwo()
}
println("得到两个值的结果是 ${one.await() + two.await()}")
}
println("两个函数调用后的消耗时间为: $time 毫秒 ")
}
//输出
得到两个值的结果是 42
两个函数调用后的消耗时间为: 2043 毫秒
不是说好通过await后可以启动协程么?为什么会恢复成2000毫秒了呢?
因为: 如果只是调用await
,而没有在单独的协程中调用start
,这将会导致顺序行为。也就是说先执行第一个await
函数,等待结果之后。再执行第二个await
。
如果我们换个变种执行:
/**
* 开始逻辑
*/
fun main() = runBlocking {
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY){
doSomethingUsefulOne()
}.await()
val two = async(start = CoroutineStart.LAZY){
doSomethingUsefulTwo()
}.await()
println("得到两个值的结果是 ${one + two}")
}
println("两个函数调用后的消耗时间为: $time 毫秒 ")
}
//
得到两个值的结果是 42
两个函数调用后的消耗时间为: 2030 毫秒
我们会发现失去了并发的效果。 上面这两种都不是推荐的惰性并发调用的推荐方法。
推荐的写法是通过调用start()
。示例:
/**
* 开始逻辑
*/
fun main() = runBlocking {
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY){
doSomethingUsefulOne()
}
val two = async(start = CoroutineStart.LAZY){
doSomethingUsefulTwo()
}
println("开始计算")
one.start()
two.start()
println("得到两个值的结果是 ${one.await() + two.await()}")
}
println("两个函数调用后的消耗时间为: $time 毫秒 ")
}
//输出
开始计算
得到两个值的结果是 42
两个函数调用后的消耗时间为: 1014 毫秒
我们如果实际运行一下,你就会发现。先输出:开始计算。让后等待差不多一秒的时间才会输出计算结果。
在上面的例子中,是我们主动给方法进行并发也就是异步计算。
那么如果该并发的函数,我们在引用的时候不知道。而没有添加async那么不是会白白造成延时吗?
针对这种特性kotlin中也有配置。
GloubalScop.async 实现函数async风格定义
我们可以通过GloubalScop.async 自己渲染函数为异步并发的函数。外部使用就当普通函数直接调用即可。
节省了在调用的时候配置async
的繁琐。
示例:
suspend fun doSomethingUsefulOneAsync() = GlobalScope.async {
delay(1000L) // 假设我们在这里做了一些有用的事
return@async 29
}
suspend fun doSomethingUsefulTwoAsync() = GlobalScope.async {
delay(1000L) // 假设我们在这里也做了一些有用的事
return@async 29
}
/**
* 开始逻辑
*/
fun main() = runBlocking {
val time = measureTimeMillis {
val one = doSomethingUsefulOneAsync()
val two = doSomethingUsefulTwoAsync()
println("开始计算")
one.start()
two.start()
println("得到两个值的结果是 ${one.await() + two.await()}")
}
println("两个函数调用后的消耗时间为: $time 毫秒 ")
}
//输出
开始计算
得到两个值的结果是 58
两个函数调用后的消耗时间为: 1036 毫秒
通过输出,我们可以看到结果是一样的。执行了并发操作。
上面的例子还有一种变种写法,我上面是用了匿名函数的跳转。下面按照正常的写法是:
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了一些有用的事
return 29
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}
// somethingUsefulOneAsync 函数的返回值类型是 Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
// somethingUsefulTwoAsync 函数的返回值类型是 Deferred<Int>
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
/**
* 开始逻辑
*/
fun main() = runBlocking {
val time = measureTimeMillis {
val one = somethingUsefulOneAsync()
val two = somethingUsefulTwoAsync()
one.start()
two.start()
println("得到两个值的结果是 ${one.await() + two.await()}")
}
println("两个函数调用后的消耗时间为: $time 毫秒 ")
}
//输出
得到两个值的结果是 58
两个函数调用后的消耗时间为: 1036 毫秒
上面这两种例子都是一样的,只是写法的区别而已。
这两种方法通过GloubalScop
调用的方法,就和其他编程语言中的异步函数的逻辑是不是看着很像?但是这种异步方式在Kotlin
中,属于强烈不推荐的写法。
因为如果 val one = somethingUsefulOneAsync()
这一行和 one.await()
表达式这里在代码中有逻辑错误, 并且程序抛出了异常以及程序在操作的过程中进行了终止。 通常情况下,一个全局的异常处理者会捕获这个异常,将异常打印成日记并报告给开发者,但是反之该程序将会继续执行其它操作。但是这里我们的 somethingUsefulOneAsync
仍然在后台执行, 尽管如此,启动它的那次操作也会被终止。
简单概括就是:这种异步结果,如果在中间发生了异常后,这个并发协程数据还是会在后台执行。存在安全隐患。
所以,针对这种需求,Kotlin建议我们使用结构化并发,也就是使用 coroutineScope
。
示例:
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了一些有用的事
return 29
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
/**
* 开始逻辑
*/
fun main() = runBlocking {
val time = measureTimeMillis {
println("得到两个值的结果是 ${concurrentSum()}")
}
println("两个函数调用后的消耗时间为: $time 毫秒 ")
}
//输出
得到两个值的结果是 58
两个函数调用后的消耗时间为: 1022 毫秒
这种情况下如果一个子协程(并发分支)出现了失败。那么其他的async和等待的父协程都会被取消。
示例:
fun main() = runBlocking<Unit> {
try {
failedConcurrentSum()
} catch(e: ArithmeticException) {
println("Arithmetice 错误:计算错误了")
}
}
suspend fun failedConcurrentSum(): Int = coroutineScope {
val one = async<Int> {
try {
delay(Long.MAX_VALUE) // 模拟一个长时间的运算
42
} finally {
println("第一个并发还么结束,但是被主动取消了")
}
}
val two = async<Int> {
println("第二个并发出现了异常,抛出一个错误")
throw ArithmeticException()
}
one.await() + two.await()
}
//输出
第二个并发出现了异常,抛出一个错误
第一个并发还么结束,但是被主动取消了
Arithmetice 错误:计算错误了
这个例子就是,在第二个协程并发中,主动触发了异常错误。然后自动帮我们取消了还在等待中的一个并发协程。
最后整个协程都进行了停止。
所以,协程尽量使用格式化方案coroutineScope
评论区