Kotlin学习6—神奇的协程

    科技2022-08-23  103

    前言

    在Kotlin中,有个非常特色的一项技术,那就是协程

    什么是协程呢?协程与线程是有点类似的,可以简单地认为协程就是一种轻量级的线程

    在平常开发的时候,线程是最小的执行单位,都知道线程是非常重量级的,需要依靠操作系统的调度才能实现不同线程之间的切换

    但是协程不同于线程的一点在于,协程可以仅在编程语言的层面上就可以实现不同协程之间的切换,从而大大提高了并发编程的运行效率

    协程允许在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,与操作系统无关

    基本用法

    由于Kotlin并没有把协程纳入到标准API中,以依赖库的形式存在,所以当使用协程的时候,我们需要在app/build.gradle中添加相关的依赖:

    // kotlin协程的核心依赖库 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" // Android项目中使用kotlin时,才添加 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"

    那么如何开启一个协程呢?最简单的方式就是使用Global.launch函数:

    @JvmStatic fun main(args: Array<String>) { GlobalScope.launch { println("hello coroutine") } } 打印结果:

    该GlobalScope.launch函数可以创建一个协程的作用域,这样传递给launch函数的代码块就是在协程中运行的了,但上述代码在运行的时候,是不会打印出任何日志。这是为什么呢?因为Global.launch函数每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟这一起结束

    所以解决这个问题,我们可以让程序延迟一段时间再结束,修改如下:

    @JvmStatic fun main(args: Array<String>) { GlobalScope.launch { println("hello coroutine") } Thread.sleep(1000) } 打印结果:hello coroutine

    但是这种让线程延迟的写法依旧有个问题,如果协程作用域中的代码块执行时间大于线程延迟时间,那么就会被强制中断,例如如下代码:

    @JvmStatic fun main(args: Array<String>) { GlobalScope.launch { println("hello coroutine") // 非阻塞式挂起函数,仅挂起当前协程,不影响其他协程的工作 // 因此,delay()函数只能在协程作用域或者其他挂起函数中使用 delay(1500) println("end coroutine") } // 会阻塞当前线程,这样在该线程下的所有协程都会被阻塞 Thread.sleep(1000) } 打印结果:hello coroutine

    由于让协程挂起1.5s,但线程却只阻塞了1s,所以会发现只打印了一条日志,因为协程的第二条日志还没来得及执行,程序就已经结束了

    那么有没有办法让应用程序在协程中所有代码块执行完毕后再结束呢?在这我们可以借助runBlocking函数来解决这个问题

    @JvmStatic fun main(args: Array<String>) { runBlocking { println("hello coroutine") delay(1500) println("end coroutine") } } 打印结果: hello coroutine end coroutine

    如何创建多个协程呢?可以用到launch函数,如下例子:

    @JvmStatic fun main(args: Array<String>) { runBlocking { launch { println("launch1") delay(1000) println("end launch1") } launch { println("launch2") delay(1000) println("end launch2") } } } 打印结果: launch1 launch2 end launch1 end launch2

    launch函数与GlobalScope.launch函数不同,launch函数必须在协程的作用域中才能调用,其次会在当前协程的作用域中创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程都会一同结束。

    从上述例子中,可以看出两个子协程中的日志是交互打印的,说明可以像多线程那样并发运行。但是这两个子协程实际是运行在一个线程中的,只是由编程语言控制其多个协程之间的调度。

    当我们需要在launch函数中编写复杂的逻辑的时候,我们需要将其中的逻辑提取出一个单独的函数,但是这样会有一个问题,我们在launch函数中编写的代码是有协程作用域的,如果提取到一个单独的函数中去的话,就没有协程作用域了,那这时我们如果调用像delay这样的挂起函数呢?这时可以借助suspend关键字,该关键字可以将任意函数声明为挂起函数,挂起函数之间是可以互相调用的

    suspend fun suspendTest() { println("this is suspend method") delay(1000) }

    注意: suspend关键字只能声明该函数是挂起函数,但无法给它提供协程作用域

    如果想在一个单独的挂起函数中使用到协程作用域的话,此时我们可以使用coroutineScope函数来解决,该函数也是一个挂起函数,可以在任何挂起函数中调用,其特点是继承外部的协程作用域并创建一个子作用域,因此我们可以给任意挂起函数提供协程作用域了

    suspend fun coroutineScopeTest() = coroutineScope { launch { println("this is coroutineScope method") delay(1000) } }

    coroutineScope函数与runBlocking函数有个类似的特点,其可以保证作用域的所有代码和子协程在全部执行完成之前,会一直阻塞当前协程,不影响其他协程,也不影响线程

    而runBlocking函数会阻塞当前线程

    fun main() { runBlocking { launch { for (i in 11..20) { println(i) delay(1000) } println("normal launch end") } coroutineScope { launch { for (i in 1..10) { println(i) delay(1000) } println("coroutineScope launch end") } } println("coroutineScope end") } println("runBlocking end") }

    更多的作用域构建器

    当我们在项目开发中,遇到一个协程进行网络请求的场景,但此时其外层的Activity已经销毁了,这时我们需要关闭这次网络请求,或者不进行回调,那么此时协程如何取消呢?

    Kotlin不管是GlobalScope.launch还是launch函数,都会返回一个Job对象,我们只需要调用Job对象的cancel函数就可取消协程了

    val job = GlobalScope.launch { } job.cancel()

    但是我们想在退出程序后,关闭所有协程的时候,如果依旧采用逐个调用已创建协程的cancel函数,那么这将是一个非常的工作量,因此,我们可以这么写:

    fun cancelTest() { val job = Job() val scope = CoroutineScope(job) scope.launch { // 协程作用域1 } scope.launch { // 协程作用域2 } scope.cancel() }

    这样创建的协程,都会被关联在Job对象的作用域下面,当调用其cancel函数后,就会将同一作用域下的所有协程全部取消,大大降低协程管理的成本

    现在我们知道调用了launch函数可以创建一个协程,但是launch函数只能用于执行一段逻辑,不能获取其执行结果,那么我们想获取其返回值的话,该怎么办呢?这时我们使用async函数

    async函数必须要在协程作用域中才能被调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数的执行结果,只需调用Deferred对象的await方法即可

    fun asyncTest() { runBlocking { val result = async { 6 + 6 } println("the result is ${result.await()}") } // 打印结果:the result is 12 }

    关于async函数,在调用其之后,代码块中的代码就会立即执行,当调用await函数时,如果代码块中的代码还没执行完,那么await函数就会将当前协程阻塞,直到可以获得async的执行结果

    关于这点,我们看个例子

    fun asyncTest1() { runBlocking { val startTime = System.currentTimeMillis() val result1 = async { delay(1000) 6 + 6 }.await() val result2 = async { delay(1000) 7 + 7 }.await() println("result is ${result1 + result2}") val endTime = System.currentTimeMillis() println("the cost ${endTime - startTime} ms") } // 打印结果: // result is 26 // the cost 2025 ms }

    使用两个async函数来执行任务,可以看到这两个async函数确实是一个串行执行的关系,这种写法的效率是非常低的,因此我们可以让两个async函数同时执行来提高运行效率,优化后的代码如下:

    fun asyncTest2() { runBlocking { val startTime = System.currentTimeMillis() val result1 = async { delay(1000) 6 + 6 } val result2 = async { delay(1000) 7 + 7 } println("result is ${result1.await() + result2.await()}") val endTime = System.currentTimeMillis() println("the cost ${endTime - startTime} ms") } // 打印结果: // result is 26 // the cost 1078 ms }

    优化后的代码不在每次调用async函数后就立刻使用await函数获取执行结果,而是仅在需要的时候才去调用其await函数来获取,这样就可以将两个async函数从串行执行变为并行执行

    最后,学习一个比较特殊的作用域构建器withContext函数。withContext函数也是一个挂起函数,可以理解为async函数的简化版

    fun withContextTest() { runBlocking { val result = withContext(Dispatchers.Default) { 6 + 6 } println(result) } // 打印结果:12 }

    调用withContext函数后,会立即执行代码块中的代码,同时也会将协程阻塞。当代码块中的代码执行完成后,会将最后一行的执行结果作为返回值返回,相当于val result = async{6+6}.await()

    但最大的不同点在于,withContext函数需要我们指定一个线程参数,表示在哪种运行线程中执行

    协程是一个轻量级线程的概念,在传统的编程情况下,我们是开启多个线程来执行并发任务的,现在只需要在一个线程中开启多个协程来执行即可。意味着我们使用协程时,外层依旧需要在线程下才能执行

    withContext函数的线程参数主要有3种值:Dispatchers.Default、Dispatchers.IO和Dispatchers.Main

    Dispatchers.Default表示会使用一种默认低并发的线程策略;Dispatchers.IO表示会使用一种较高并发的线程策略;Dispatchers.Main表示不回开启子线程,而是在Android中执行代码,这个值只能在Android中使用

    suspendCoroutine函数

    优点:大幅简化传统的回调机制的写法

    作用:必须在协程作用域或者挂起函数中使用,其接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通线程里面执行Lambda表达式中的代码。在Lambda表达式中会传入一个Continuation参数,调用其resume()方法或resumeWithException()可以让协程恢复执行

    缺点:存在局限性,需要结合协程一块使用,只能用在协程作用域或者挂起函数中

    例子:优化前:

    fun request1() { HttpUtil.sendHttpRequest(address, object : HttpCallbackListener) { override fun onFinish(response: String) { // 成功请求,并得到数据返回 } override fun onError(e: Exception) { // 请求失败,返回异常 } } }

    使用suspendCoroutine函数优化后,

    suspend fun request2(address: String): String { // 使用suspendCoroutine,会立即挂起当前协程,并将Lambda表达式中的逻辑在普通线程中执行 return suspendCoroutine { continuation -> HttpUtil.sendHttpRequest(address, object : HttpCallbackListener) { override fun onFinish(response: String) { // 成功请求,并得到数据返回 continuation.resume(response) } override fun onError(e: Exception) { // 请求失败,返回异常 continuation.resumeWithException(e) } } } }

    使用优化后的网络请求,仅需简单使用即可,

    suspend fun getNetworkRequest() { try { val result = request2(url) // 对返回的数据进行处理 } catch (e: Exception) { // 对异常进行处理 } }

    由于getNetworkRequest()是一个挂起函数,因此当它调用request函数时,当前的协程会被立即挂起,然后一直等待网络请求成功或者失败后,当前协程才会恢复运行,这样不使用回调的写法,也能得到异步网络请求的响应数据,这就是Kotlin协程的独特之处

    Processed: 0.016, SQL: 9