侧边栏壁纸
博主头像
Z同学博主等级

工作磨平激情前,坚持技术的热忱。 欢迎光临Z同学的技术小站。 分享最新的互联网知识。

  • 累计撰写 290 篇文章
  • 累计创建 57 个标签
  • 累计收到 98 条评论

Kotlin 协程 组合挂起函数和async关键字,实现协程的并发操作

Z同学
2021-12-13 / 0 评论 / 0 点赞 / 576 阅读 / 6,144 字
温馨提示:
本文最后更新于 2021-12-13,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

介绍

主要是学习如何在协程之中使用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

0

评论区