Kotlin 协程使用及其详解

Kotlin协程,好用,但是上限挺高的,我一直感觉自己就处于会用,知其然不知其所以然的地步。

做点小总结,比较浅显。后面自己再继续补充吧。

一、什么是协程?

Kotlin 协程是一种轻量级的并发编程方式,用于简化异步代码的编写。它允许你编写看似同步的代码来处理异步任务,使代码更加简洁、可读且易于维护。协程广泛应用于 Android 开发中,用于网络请求、数据库操作等需要异步执行的任务。

  • 协程是一种语法糖 协程的出现是来解决异步问题的,但它本身却不提供异步的能力,协程某种意义上更像是一种语法糖,它为我们隐藏了异步调用和回调的细节,让我们更关注于业务逻辑的实现。
  • 一句话概括,协程是一种轻量级的方便操作异步代码的语法糖,而它本身不提供异步能力。

二、Kotlin 协程的核心概念

  • 协程 (Coroutine):一种轻量级的线程。协程可以在不阻塞主线程的情况下挂起和恢复,使得代码能够异步执行而不增加线程开销。协程在 Kotlin 中由 suspend 函数来支持,避免回调地狱的情况。

  • 挂起函数 (suspend function):使用 suspend 关键字修饰的函数,可以在协程中挂起。挂起函数不会阻塞线程,而是挂起当前协程,直到任务完成后再继续执行。例如:

suspend fun fetchData(): String {
    // 模拟网络请求
    delay(1000)  // 挂起 1 秒,不会阻塞线程
    return "Data fetched"
}
  • 挂起 (Suspend) 和恢复 (Resume):当协程执行到 delaywithContext 等挂起函数时,它会暂停执行并释放资源,一旦挂起函数完成任务,协程会恢复执行。这种机制使得协程能够在同一线程上无缝切换,从而提高性能。

  • 作用域 (CoroutineScope):协程作用域定义了协程的生命周期。常见的协程作用域有 GlobalScopeCoroutineScopeviewModelScopelifecycleScope,它们决定了协程的启动与取消。作用域有助于自动管理协程,确保在不再需要协程时取消它,避免资源泄漏。

  • 上下文 (CoroutineContext):协程的上下文包含了协程的调度器(比如 Dispatchers.MainDispatchers.IO)和其他信息。上下文指定了协程在哪个线程或线程池上执行,调度器为协程提供执行环境。

三、协程的特点

  • 轻量

    • 一个线程中可以包含多个协程,协程支持挂起,不会让正在运行协程的线程阻塞,与阻塞线程相比,挂起协程的操作更轻量
  • 内存泄漏更少
    • 协程使用了结构化并发机制,可以在一个作用域内执行多个操作,可以一次性全部取消掉,这样就不用像 RxJava 一样要自己把 Disposable 放在 CompositeDisposable 里
  • 内置取消支持
    • 当我们取消一个协程时,取消操作会在运行中的整个协程层次结构内传播,也就是父协程取消后,子协程也会被取消
  • Jetpack支持
    • 集成 Jetpack 中的 ViewModel 、Lifecycle 和 LiveData 都提供了对应的协程作用域
  • Kotlin 协程框架中的挂起函数有另外一个好处,就是可以在编译时就让方法的调用方知道这是一个耗时的操作,需要确定这个操作要放在哪个线程执行,这样就不用像 Android 框架对主线程网络请求的禁止方式一样,在运行时才抛出异常。

四、协程和线程的区别

(1)协程是编译器级别的,线程是系统级别的。协程的切换是由程序来控制的,线程的切换是由操作系统来控制的。

(2)协程是协作式的,线程是抢占式的。协程是由程序来控制什么时候进行切换的,而线程是有操作系统来决定线程之间的切换的。

(3)一个线程可以包含多个协程。

(4)Java中,多线程可以充分利用多核cpu,协程是在一个线程中执行。

(5)协程适合io密集型的程序,多线程适合计算密集型的程序(适用于多核cpu的情况)。当你的程序大部分是文件读写操作或者网络请求操作的时候,这时你应该首选协程而不是多线程,首先这些操作大部分不是利用cpu进行计算而是等待数据的读写,其次因为协程执行效率较高,子程序切换不是线程切换,是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

(6)使用协程可以顺序调用异步代码,避免回调地狱。

五、协程的使用

Kotlin 提供了丰富的协程构建器,如 launchasync,分别用于启动协程并发任务。

1. launch

launch 是一种最常用的协程构建器,用于启动一个新协程,并且不会阻塞主线程。它的返回值是 Job,可以用于取消协程。

GlobalScope.launch {
    val result = fetchData()
    println(result)
}
2. async

async 用于并发执行多个任务,适合需要返回结果的情况。它的返回值是 Deferred,可以通过 await() 来获取执行结果。

CoroutineScope(Dispatchers.Main).launch {
    val result1 = async { fetchData() }
    val result2 = async { fetchData() }
    println(result1.await() + result2.await())
}

六、Job的使用

我们在使用launch的时候,就启动了一个协程,launch方法会返回一个job。

1.使用job.cancel()取消一个协程

fun main() {
    val job = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    job.cancel()
    println("Hello,")

}

因为协程被取消了,所以只会打印Hello。

2、join()等待协程执行完毕

作用类似于Thread.join()函数,join()后面的代码会等到协程结束再执行,结果如下:

fun main() = runBlocking {
    val job = GlobalScope.launch {
        delay(1000L)
        println("World!")
        delay(1000L)
    }
    println("Hello,")
    job.join() 
    println("Good!")
}

//依次打印
Hello,
World!
Good!

七、 其他注意事项

在Activity或Fragment中使用协程时,要尽量避免使用GlobalScope,因为GlobalScope是生命周期是process级别的,所以上面的例子中,即使Activity或Fragment已经被销毁,协程仍然在执行。

建议使用具有生命周期协程LifecycleScope。

关于kotlin相关的同步,异步,回调,阻塞,这篇文章很生动:

https://juejin.cn/post/7373502637729513506https://juejin.cn/post/7373502637729513506retrofit搭配协程

https://juejin.cn/post/6962921891501703175https://juejin.cn/post/6962921891501703175emmm...感觉写的很浅,还有很多知识点没有概括到,后面补充

上一篇:打印沙漏的4种解法(直接法编程、艺术化编程)


下一篇:React Native WebView 进阶:实现带回调函数的通讯