Kotlin协程详解——协程基础

目录

一、第一个协程程序

为什么使用协程?

二、结构化并发

三、提取函数重构

四、作用域构建器

五、协程句柄Job

六、协程很轻量


一、第一个协程程序

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。所以并不是一些人所说的什么线程的另一种表现。虽然协程的内部也使用到了线程。但它更大的作用是它的设计思想。将我们传统的Callback回调方式进行消除。将异步编程趋近于同步对齐。

协程优点

  1. 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。

  2. 内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作。

  3. 内置取消支持:取消功能会自动通过正在运行的协程层次结构传播。

  4. Jetpack集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

协程是一个可挂起的计算实例。从概念上讲,它与线程类似,因为它也运行一段代码,并且与其余代码并发执行。然而,协程并不绑定到任何特定的线程。它可以在一个线程中挂起执行,并在另一个线程中恢复执行。

协程可以被视为轻量级的线程,但它们之间存在一些重要的区别,这使得它们在实际使用中与线程有很大的不同。

协程是一种可以挂起和恢复执行的计算。与线程不同,协程的挂起和恢复不需要操作系统的介入,因此协程的开销非常小。这使得我们可以在一个程序中同时运行大量的协程,而不会像线程那样消耗大量的系统资源。

运行以下代码来创建你的第一个可工作的协程:

import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
//sampleEnd

代码运行的结果:
Hello
World!

让我们来剖析这段代码的作用:  

  • launch 是一个协程构建器。它会启动一个新的协程,与代码的其余部分并发运行,而其余代码会继续独立执行。这就是为什么 Hello 会首先被打印出来。
  • delay 是一个特殊的挂起函数。它会使协程挂起指定的时间。挂起协程并不会阻塞底层线程,而是允许其他协程运行并使用底层线程来执行它们的代码。
  • runBlocking 也是一个协程构建器,它充当了常规函数 main() 的非协程世界与 runBlocking { ... } 大括号内包含协程的代码之间的桥梁。这在集成开发环境(IDE)中通过 runBlocking 开括号后的 CoroutineScope 提示来突出显示。

如果在代码中删除或忘记使用 runBlocking,那么在调用 launch 时会出现错误,因为 launch 仅在 CoroutineScope 上声明:

Unresolved reference: launch

runBlocking 的名称意味着运行它的线程(在这种情况下是主线程)在调用期间会被阻塞,直到 runBlocking { ... } 内的所有协程都完成执行。你通常会看到 runBlocking 在应用程序的最顶层被这样使用,而在真正的代码中却很少见到它的身影,因为线程是宝贵的资源,阻塞它们是低效的,而且通常是不被期望的。

为什么使用协程?

简单的来说,Coroutine是一个并发的设计模式,你能通过它使用更简洁的代码来解决异步问题。

例如,在Android方面它主要能够帮助你解决以下两个问题:

  1. 在主线程中执行耗时任务导致的主线程阻塞,从而使App发生ANR。

  2. 提供主线程安全,同时对来自于主线程的网络回调、磁盘操提供保障。

这些问题,在接下来的文章中我都会给出解决的示例。

Callback

说到异步问题,我们先来看下我们常规的异步处理方式。首先第一种是最基本的callback方式。

callback的好处是使用起来简单,但你在使用的过程中可能会遇到如下情形

1        GatheringVoiceSettingRepository.getInstance().getGeneralSettings(RequestLanguage::class.java)
2                .observe(this, { language ->
3                    convertResult(language, { enable -> 
4                        // todo something
5                    })
6                })

这种在其中一个callback中回调另一个callback回调,甚至更多的callback都是可能存在。这些情况导致的问题是代码间的嵌套层级太深,导致逻辑嵌套复杂,后续的维护成本也要提高,这不是我们所要看到的。

那么有什么方法能够解决呢?当然有,其中的一种解决方法就是我接下来要说的第二种方式。

Rx系列

对多嵌套回调,Rx系列在这方面处理的已经非常好了,例如RxJava。下面我们来看一下RxJava的解决案例

1        disposable = createCall().map {
2            // return RequestType
3        }.subscribeWith(object : SMDefaultDisposableObserver<RequestType>{
4            override fun onNext(t: RequestType) {
5                // todo something
6            }
7        })

RxJava丰富的操作符,再结合Observable与Subscribe能够很好的解决异步嵌套回调问题。但是它的使用成本就相对提高了,你要对它的操作符要非常了解,避免在使用过程中滥用或者过度使用,这样自然复杂度就提升了。

那么我们渴望的解决方案是能够更加简单、全面与健壮,而我们今天的主题Coroutine就能够达到这种效果。

二、结构化并发

协程遵循结构化并发的原则,这意味着新的协程只能在特定的 CoroutineScope 内启动,该作用域限定了协程的生命周期。上面的例子表明,runBlocking 建立了相应的作用域,这就是为什么前一个例子会等待一秒延迟后打印出 "World!",然后才退出。

在真实的应用程序中,你会启动大量的协程。结构化并发确保它们不会被遗漏或泄露。一个外部作用域在其所有子协程完成之前不能完成。结构化并发还确保代码中的任何错误都能得到正确的报告,并且永远不会丢失。

三、提取函数重构

让我们将 launch { ... } 内的代码块提取到一个单独的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有 suspend 修饰符的新函数。这是你的第一个挂起函数。挂起函数可以在协程内部像普通函数一样使用,但它们的额外功能是能够反过来使用其他挂起函数(如本例中的 delay),以挂起协程的执行。

import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
//sampleEnd

四、作用域构建器

除了不同构建器提供的协程作用域外,还可以使用 coroutineScope 构建器声明自己的作用域。coroutineScope 创建一个协程作用域,并会一直等待,直到所有启动的子协程都完成。coroutineScope实际上会挂起(suspend)调用它的那个协程。

runBlocking 和 coroutineScope 构建器可能看起来相似,因为它们都会等待其主体和所有子协程完成。主要区别在于,runBlocking 方法会阻塞当前线程以进行等待,而 coroutineScope 只是挂起,释放底层线程以供其他用途。正因为这种差异,runBlocking 是一个普通函数,而 coroutineScope 是一个挂起函数。

你可以从任何挂起函数中使用 coroutineScope。例如,你可以将“Hello”和“World”的并发打印移动到挂起函数 doWorld() 中:

import kotlinx.coroutines.*
//sampleStart
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
//sampleEnd

执行结果为:

Hello
World!

五、作用域构建器与并发

coroutineScope 构建器可以在任何挂起函数内部使用,以执行多个并发操作。让我们在 doWorld 挂起函数内部启动两个并发协程:

import kotlinx.coroutines.*
//sampleStart
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
doWorld()
println("Done")
}
// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
//sampleEnd

launch { ... } 块内的两段代码都会并发执行。从程序开始计时,首先会在1秒后打印出“World 1”,紧接着在2秒后打印出“World 2”。doWorld 函数中的 coroutineScope 只有在两个协程都完成后才会结束,因此 doWorld 函数只有在此时才会返回,并允许打印出“Done”字符串:

执行结果为:

Hello
World 1
World 2
Done

五、协程句柄Job

launch 协程构建器返回一个 Job 对象,这个对象是已启动协程的句柄,可以用来显式地等待其完成。例如,你可以等待子协程完成,然后打印出“Done”字符串:

import kotlinx.coroutines.*
fun main() = runBlocking {
//sampleStart
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")
//sampleEnd
}

执行结果为:

Hello
World!
Done

六、协程很轻量

协程相比JVM线程使用的资源更少。使用线程时可能导致JVM可用内存耗尽的代码,在使用协程时则可以避免达到资源限制。例如,下面的代码启动了50,000个不同的协程,每个协程等待5秒钟,然后打印一个点('.'),同时消耗非常少的内存:

fun main() = runBlocking {
repeat(50_000) { // 启动大量的协程
launch {
delay(5000L)
print(".")
}
}
}

如果你使用线程来编写相同的程序(移除runBlocking,将launch替换为thread,并将delay替换为Thread.sleep),那么它将消耗大量内存。这取决于你的操作系统、JDK版本及其设置,程序可能会抛出内存溢出错误,或者线程启动速度会变得缓慢,以确保不会有太多线程同时运行。

推荐文章

Welcome to our tour of Kotlin! | Kotlin Documentation

https://zhuanlan.zhihu.com/p/613052280

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闲暇部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值