Kotlin协程学习--组合挂起函数

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情

组合挂起函数

下面的学习笔记根据官网的文档学习了将挂起函数进行组合的各种方式。官网文档的地址:组合挂起函数

默认顺序调用

假设我们在不同的地方定义了两个进行某种调用远程服务或者进行某种计算的挂起函数,下面的例子中这两个函数仅仅是做了延时的操作,我们假设它们是有用的。

    suspend fun doSomethingOne(): Int{
        delay(1000L)
        return 13
    }

    suspend fun doSomethingTwo(): Int{
        delay(1000L)
        return 10
    }

如果我们想要顺序调用这两个方法,我们就可以直接先调用doSomethingOne()然后调用doSomethingTwo(),和调用普通的函数是一样的。下面的代码在调用这两个函数的同时还测量了执行的时间,如下所示:

    fun doSomething() = runBlocking {
        val time = measureTimeMillis{
            val one = doSomethingOne()
            val two = doSomethingTwo()
            val result = one + two
            LogUtils.e("result is: $result")
        }
        LogUtils.e("time is: $time")
    }
    
    //打印的日志如下:
    15:02:32:095    TAG_ZYF:	result is: 23
    15:02:32:095    TAG_ZYF:	time is: 2021

可以看到:我们在RunBlocking中顺序调用了两个挂起函数,最后对这两个挂起函数的返回值做了相加的操作,程序是顺序执行的,相加操作的结果也是对的。另外我们计算了执行这段代码的总时间,可以看到时间也是没有问题的。

使用async执行并发

如果我们的代码中对于doSomethingOne()doSomethingTwo()的结果没有任何关系,我们只是想要快速得到这两个函数的执行结果,那么我们就可以使用async并发执行这两个函数,如下所示:

    /**
     * 使用[async]并发执行[doSomethingOne]和[doSomethingTwo]这两个挂起函数
     */
    fun doSomethingAsync() = runBlocking {
        val time = measureTimeMillis {
            val one = async { doSomethingOne() }
            val two = async { doSomethingTwo() }
            LogUtils.e("time end,one is ${one.await()},two is ${two.await()}")
        }
        LogUtils.e("time is: $time")
    }
    
    //打印日志信息如下:
    15:13:27:722    TAG_ZYF:	time end,one is 13,two is 10
    15:13:27:723    TAG_ZYF:	time is: 1015

可以看到,这次执行两个函数总共花费了一秒多的时间。

在概念上,async就类似于launch。它启动了一个单独的协程,这是一个轻量级的线程并于其它所有的协程一起并发工作。不同之处在于,launch返回一个job并且不附带任何返回值,而async返回一个Deferred,这是一个轻量级的非阻塞feature,代表了一个将会在稍后提供结果的promise。我们可以使用.await()在一个延期的值上得到它的最终结果。不过Deferrer本质上也是一个Job,我们仍然可以在需要的时候取消它。

惰性启动的async

async可以通过将参数中的start设置为CoroutineStart.LAZY从而变为惰性的。在这种模式下,只有结果通过await()获取的时候才会启动协程,或者在Jobstart()函数调用的时候才会启动协程,如下所示:

    /**
     * 惰性[async]
     */
    fun doSomethingAsyncLazy() = runBlocking {
        val time = measureTimeMillis {
            val one = async(start = CoroutineStart.LAZY) { doSomethingOne() }
            val two = async(start = CoroutineStart.LAZY) { doSomethingTwo() }
            //开始第一个计算
            one.start()
            //开始第二个计算
            two.start()
            //输出计算结果
            LogUtils.e("end,one is ${one.await()},two is ${two.await()}")
        }
        LogUtils.e("time is: $time")
    }
    
    //打印日志如下:
    15:30:02:180    TAG_ZYF:	end,one is 13,two is 10
    15:30:02:180    TAG_ZYF:	time is: 1015

上面的例子可能看的不是很清晰,我们稍微修改一下其中的代码逻辑:

    /**
     * 惰性[async]
     */
    private fun doSomethingAsyncLazy() = runBlocking {
        val time = measureTimeMillis {
            val one = async(start = CoroutineStart.LAZY) {
                doSomethingOne()
            }
            val two = async(start = CoroutineStart.LAZY) {
                doSomethingTwo()
            }
            delay(2000L)
            //开始第一个计算
            one.start()
            //开始第二个计算
            two.start()
            delay(2000L)
            //输出计算结果
            LogUtils.e("end,one is ${one.await()},two is ${two.await()}")
        }
        LogUtils.e("time is: $time")
    }
    
    //打印的日志如下:
    15:39:46:929    TAG_ZYF:	programing start
    15:39:48:994    TAG_ZYF:	one start
    15:39:48:994    TAG_ZYF:	two start
    15:39:51:000    TAG_ZYF:	end,one is 13,two is 10
    15:39:51:000    TAG_ZYF:	time is: 4023
    15:39:51:001    TAG_ZYF:	programing end

可以看到,我们在程序启动后等待了2秒的时间才启动了两个挂起函数的行为,之后继续等待2秒输出了两个挂起函数的执行结果。说明在调用start()之前并没有启动挂起函数,调用start()之后开始启动挂起函数执行。

需要注意的是,如果我们只是在输出的时候调用了await(),而没有在单独的协程中调用start(),这将会导致顺序行为,直到await()启动该协程执行并等待至它结束,这并不是惰性的预期用例。在计算一个值涉及挂起函数时,这个async(start = CouroutineStart.LAZY)的用例用于代替标准库中的lazy()函数。如下所示:

    /**
     * 惰性[async]
     */
    private fun doSomethingAsyncLazy() = runBlocking {
        val time = measureTimeMillis {
            val one = async(start = CoroutineStart.LAZY) {
                doSomethingOne()
            }
            val two = async(start = CoroutineStart.LAZY) {
                doSomethingTwo()
            }
            val oneResult = one.await()
            val twoResult = two.await()
            //输出计算结果
            LogUtils.e("end,one is $oneResult,two is $twoResult")
        }
        LogUtils.e("time is: $time")
    }
    
    //打印的日志如下:
    15:50:14:572    TAG_ZYF:	one start
    15:50:15:584    TAG_ZYF:	two start
    15:50:16:595    TAG_ZYF:	end,one is 13,two is 10
    15:50:16:595    TAG_ZYF:	time is: 2032

从输出的信息可以看出,在使用了async(start = CoroutineStart.LAZY)之后,我们的并发执行的特性好像消失了,上面的程序似乎和本片笔记中的第一个示例一样了变成了顺序执行了。所以需要注意在使用惰性async的时候,应该在单独的协程中调用start()方法从而保持并发执行。

使用async结构化并发

我们可以单独提取出一个函数用于计算doSomethingOne()doSomethingTwo()两个函数返回结果的和。仍然使用async让这两个函数并发执行,不过由于async被定义为了CoroutineScope上的扩展,我们需要将它写在作用域内,我们可以使用coroutineScope函数所提供的作用域,如下所示:

    private suspend fun doSomethingSum(): Int = coroutineScope {
        val one = async { doSomethingOne() }
        val two = async { doSomethingTwo() }
        one.await() + two.await()
    }

这样我们就可以通过调用这个挂起函数来获得结果,如下所示:

    private fun getSum() = runBlocking {
        val time = measureTimeMillis {
            val result = doSomethingSum()
            LogUtils.e("doSomethingSum is: $result")
        }
        LogUtils.e("time is $time")
    }
    
    //打印日志如下
    16:08:19:322    TAG_ZYF:	one start
    16:08:19:324    TAG_ZYF:	two start
    16:08:20:338    TAG_ZYF:	doSomethingSum is: 23
    16:08:20:338    TAG_ZYF:	time is 1026

可以看到,这里我们仍然是同时执行了这两个函数。

对于上面这种方式,还有一个好处就是取消始终通过协程的层次结构来传递,如下所示:

    private suspend fun failedSum(): Int = coroutineScope {
        val one = async<Int> {
            try {
                delay(Long.MAX_VALUE)//等待一段较长的时间
                3
            }finally {
                LogUtils.e("one canceled")
            }
        }

        val two = async<Int> {
            LogUtils.e("two will failed")
            throw ArithmeticException()
        }
        
        one.await() + two.await()
    }

在上面的代码中,正常情况下应该是计算两个async代码块执行结束的和,但是我们模拟在程序执行过程中出错的情况,第一个数尚未请求到,第二个数获取的时候出错,我们尝试调用这个方法,如下所示:

    /**
     * 获取错误的和
     */
    private fun getFailedSum() = runBlocking {
        try {
            failedSum()
        }catch (e: Exception){
            LogUtils.e("getFailedSum exception: $e")
        }
    }
    
    //打印日志如下
    16:18:12:779    TAG_ZYF:	two will failed
    16:18:12:780    TAG_ZYF:	one canceled
    16:18:12:781    TAG_ZYF:	getFailedSum exception: java.lang.ArithmeticException

可以看到错误信息传递到了getFialedSum()函数中,并且获取第一个数的async代码块也取消了执行。也就是说,如果其中一个协程失败,第一个async以及等待中的父协程都会被取消。

猜你喜欢

转载自juejin.im/post/7112044086839738382
今日推荐