Kotlin原理+协程基本使用

协程概念

协程是Coroutine的中文简称,co表示协同、协作,routine表示程序。协程可以理解为多个互相协作的程序。协程是轻量级的线程,它的轻量体现在启动和切换,协程的启动不需要申请额外的堆栈空间;协程的切换发生在用户态,而非内核态,避免了复杂的系统调用。

特点

  • 1)更加轻量级,占用资源更少。
  • 2)避免"回调地狱",增加代码可读性。
  • 3)协程的挂起不阻塞线程。

协程的描述:

  • 协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销比较大。
  • 而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。
  • 总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法---协程挂起。

Kotlin 协程的基本使用

讲概念之前,先讲用法。

场景: 开启工作线程执行一段耗时任务,然后在主线程对结果进行处理。

常见的处理方式:

  • 自己定义回调,进行处理
  • 使用 线程/线程池, Callable、线程 Thread(FeatureTask(Callable)).start、线程池 submit(Callable)
  • Android: Handler、 AsyncTask、 Rxjava

使用协程:

复制代码
coroutineScope.launch(Dispatchers.Main) { // 在主线程启动一个协程
    val result = withContext(Dispatchers.Default) { // 切换到子线程执行
        doSomething()  // 耗时任务
    }
    handResult(result)  // 切回到主线程执行
}

这里需要注意的是: Dispatchers.Main 是 Android 里面特有的,如果是java程序里面是用则会抛出异常。

创建协程的三种方式

使用 runBlocking 顶层函数创建:

复制代码
runBlocking {
    ...
}

使用 GlobalScope 单例对象创建

复制代码
GlobalScope.launch {
    ...
}

自行通过 CoroutineContext 创建一个 CoroutineScope 对象

复制代码
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    ...
}

方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。

方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会只受整个应用程序的生命周期限制,且不能取消。

方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。

等待一个作业

先看一个示例:

复制代码
fun main() = runBlocking {
    launch {
        delay(100)
        println("hello")
        delay(300)
        println("world")
    }
    println("test1")
    println("test2")
}

执行结果如下:

复制代码
test1
test2
hello
world

我们启动了一个协程之后,可以保持对它的引用,显示地等待它执行结束,注意这里的等待是非阻塞的,不会将当前线程挂起。

复制代码
fun main() = runBlocking {
    val job = launch {
        delay(100)
        println("hello")
        delay(300)
        println("world")
    }
    println("test1")
    job.join()
    println("test2")
}

输出结果:

复制代码
test1
hello
world
test2

类比 java 线程,也有 join 方法。但是线程是操作系统界别的,在某些 cpu 上,可能 join 方法不生效。

协程的取消

与线程类比,java 线程其实没有提供任何机制来安全地终止线程。

Thread 类提供了一个方法 interrupt() 方法,用于中断线程的执行。调用interrupt()方法并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。然后由线程在下一个合适的时机中断自己。

但是协程提供了一个 cancel() 方法来取消作业。

复制代码
fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: test $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: ready to cancel!")
    job.cancel() // 取消该作业
    job.join() // 等待作业执行结束
    println("main: Now cancel.")
}

输出结果:

复制代码
job: test 0 ...
job: test 1 ...
job: test 2 ...
main: ready to cancel!
main: Now cancel.

也可以使用函数 cancelAndJoin, 它合并了对 cancel 以及 join 的调用。

问题:

如果先调用 job.join() 后调用 job.cancel() 是是什么情况?

取消是协作的

协程并不是一定能取消,协程的取消是协作的。一段协程代码必须协作才能被取消。

所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。

如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的。

复制代码
fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
            // 每秒打印消息两次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: hello ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 等待一段时间
    println("main: ready to cancel!")
    job.cancelAndJoin() // 取消一个作业并且等待它结束
    println("main: Now cancel.")
}

此时的打印结果:

复制代码
job: hello 0 ...
job: hello 1 ...
job: hello 2 ...
main: ready to cancel!
job: hello 3 ...
job: hello 4 ...
main: Now cancel.

可见协程并没有被取消。为了能真正停止协程工作,我们需要定期检查协程是否处于 active 状态。

检查 job 状态

一种方法是在 while(i<5) 中添加检查协程状态的代码

代码如下:

复制代码
while (i < 5 && isActive)

这样意味着只有当协程处于 active 状态时,我们工作的才会执行。

另一种方法使用协程标准库中的函数 ensureActive(), 它的实现是这样的:

复制代码
publc fun Job.ensureActive(): Unit {
    if (!isActive) throw getCancellationException()
}

代码如下:

复制代码
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
    ensureActive()
    ...
}

ensureActive() 在协程不在 active 状态时会立即抛出异常。

使用 yield()

yield() 和 ensureActive 使用方式一样。

yield 会进行的第一个工作就是检查任务是否完成,如果 Job 已经完成的话,就会抛出 CancellationException 来结束协程。yield 应该在定时检查中最先被调用。

复制代码
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
    yield()
    ...
}

等待协程的执行的结果

对于无返回值的的协程使用 launch 函数创建,如果需要返回值,则通过 async 函数创建。

使用 async 方法启动 Deferred (也是一种 job), 可以调用它的 await() 方法获取执行的结果。

形如下面代码:

复制代码
val asyncDeferred = async {
    ...
}


val result = asyncDeferred.await()

deferred 也是可以取消的,对于已经取消的 deferred 调用 await() 方法,会抛出

JobCancellationException 异常。

同理,在 deferred.await 之后调用 deferred.cancel(), 那么什么都不会发生,因为任务已经结束了。

关于 async 的具体用法后面异步任务再讲。

协程的异常处理

由于协程被取消时会抛出 CancellationException ,所以我们可以把挂起函数包裹在 try/catch 代码块中,这样就可以在 finally 代码块中进行资源清理操作了。

复制代码
fun main() = runBlocking {
    val job = launch {
        try {
            delay(100)
            println("try...")
        } catch (e: Exception) {
            println("exception: ${e.message}")
        } finally {
            println("finally...")
        }
    }
    delay(50)
    println("cancel")
    job.cancel()
    print("Done")
}

结果:

复制代码
cancel
Doneexception: StandaloneCoroutine was cancelled
finally...

协程的超时

在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动,使用 withTimeout 函数。

复制代码
fun main() = runBlocking {
    withTimeout(300) {
        println("start...")
        delay(100)
        println("progress 1...")
        delay(100)
        println("progress 2...")
        delay(100)
        println("progress 3...")
        delay(100)
        println("progress 4...")
        delay(100)
        println("progress 5...")
        println("end")
    }
}

结果:

复制代码
start...
progress 1...
progress 2...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms

withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子类。 我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main 函数中正确地使用了 withTimeout。如果有必要,我们需要主动 catch 异常进行处理。

当然,还有另一种方式: 使用 withTimeoutOrNull。

withTimeout 是可以由返回值的,执行 withTimeout 函数,会阻塞并等待执行完返回结果或者超时抛出异常。withTimeoutOrNull 用法与 withTimeout 一样,只是在超时后返回 null 。

除了这些Kotlin的基本学习,还有许多要进阶的技术点,如下:参考《Kotlin手册笔记》可以点击查看详情类目。

最后

本文主要是对Kotlin 协程的简单概述,还有对Kotlin 协程的使用运用。更多的Kotlin的技术可以前往主页查看我的更多文章技术。

相关推荐
techdashen16 分钟前
Go设计取舍之三: 0.3ns每次的错误Benchmark
开发语言·后端·golang
龚礼鹏18 分钟前
深入解析 Android Automotive (AAOS) 启动流程与 CarService 核心机制(基于 Android 16 最新源码视角)
android
rosmis24 分钟前
agent各指标定义
android·java·开发语言
mifengxing27 分钟前
Java 集合进阶(一)
java·开发语言·数据结构·复习笔记
爱码小白1 小时前
importlib模块
开发语言·前端·python
techdashen2 小时前
Go设计取舍之四: map不变时能否并发修改不同value
开发语言·后端·golang
小小小米粒2 小时前
阿姆达尔定律(Amdahl‘s Law)
java·开发语言
知彼解己2 小时前
Java 版本演进
java·开发语言·spring boot
风样滴男人哟2 小时前
PHP特性之反射类ReflectionClass机制
android·开发语言·php