kotlin协程:概念与入门

前言

使用 kotlin 协程已经几年了,可以说它极大地简化了多线程问题的复杂度,非常值得学习和掌握。此文介绍并梳理协程的相关概念:suspend、non-blocking、Scope、Job、CoroutineContext、Dispatchers 和结构化并发。

进入协程世界

简而言之,协程是可以在其内部进行挂起操作的实例,是否支持挂起函数也是协程世界和非协程世界的最大区别。初学者可以把协程看作是"轻量级线程"以做对比,但实际上他依然是跑在线程上的,所以也可以将它看作是一个强大的异步框架。

要使用协程,需要添加 kotlinx-coroutines-core 库的依赖。

挂起函数与非阻塞

先看一段代码:

kotlin 复制代码
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    println("Hello World!")
}

runBlocking 是一个协程构造器,他连接了非协程和协程世界,{ } 里便是协程世界。这两个世界的差异在于是否可支持挂起操作:

kotlin 复制代码
import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
    println("Hello World!")
}

"Hello World!" 将会在进入协程 1s 后打印,delay 便是一个挂起函数,类似线程中 sleep 的作用。区别是挂起函数 delay 并不会阻塞当前线程。这里有两个概念,挂起和非阻塞,挂起的是协程,非阻塞的是线程。

从线程的角度看,假如进入协程时运行在线程 A,那么执行 delay 函数时运行在线程 B,执行完 delay 之后又在线程 C 上执行 println,线程 C 可能就是线程 A,也可能不是,整个过程可以简化为:在某个线程执行协程,在遇到挂起函数 delay 时切换到另一个线程,执行完 delay 后又切回某个线程继续执行协程。不阻塞线程即不阻塞挂起前协程所在的线程,即 A 线程,A 线程可以继续执行其他任务。那这有什么意义呢?试想协程一开始运行在 Android 中的 ui 线程,在挂起函数里执行耗时的网络请求,网络请求结束自动回到协程,继续在 ui 线程上的执行。一方面耗时任务未阻塞 ui 线程,另一方面完全消除了异步回调,这使异步任务变得极为简单:以同步的方式书写异步的代码。

从协程的角度看,协程遇到挂起函数时会被挂起,即暂停了,等待挂起函数执行完成,相比于线程阻塞,协程挂起几乎没有任何资源消耗,其本质上是回调。

另外,挂起函数都有 suspend 关键字修饰,编译器也会在此施加魔法:

kotlin 复制代码
public suspend fun delay(timeMillis: Long) { ... }

注:例子中的线程 A、B、C 是通过协程中的 Dispatcher 控制的,后面聊到。

CoroutineScope、Job 与 Context

协程都是由 CoroutineScope 创建的(launch、async 等),协程在创建时,都会关联到一个新的的 CoroutineScope

CoroutineScope 即协程作用域,顾名思义是为了限制和控制协程的作用范围或者说生命周期。不仅当前协程会受其影响,所有在协程作用域内创建的子协程也会有关联。当调用 CoroutineScope 的 cancel 方法时,会取消当前协程以及其关联的所有下层协程。

通过 CoroutineScope 创建的协程即 Job,可以认为 Job 就是协程的实例。一个 Job 可以有多个子 Job,也即一个协程可以有多个子协程。具有父子关系的协程,父协程取消时,所有子协程都会取消,CoroutineScope 的 cancel 本质就是通过取消 Job 实现的,因此 CoroutineScope 的 cancel 等价于 Job 的 cancel。

kotlin 复制代码
val job = launch { // 1
    launch { // 2
        ...
    }
    
    launch { // 3
        ...
    }
}
job.cancel()

上面代码中 job 取消时会把 2、3处协程也取消。

协程层次化的好处是便于管理,再多的协程,只要它们具有相同的父协程,就可以方便地控制其生命周期。在层次化的协程结构中,取消事件自上而下,异常事件自下而上,这背后是结构化并发的思想。

CoroutineContext ,协程上下文,是 CoroutineScope 的唯一成员,是用于存放协程执行环境的地方,如调度器(Dispatcher)、异常处理器(CoroutineExceptionHandler)、Job 等。

CoroutineScope 在创建协程时会把 CoroutineContext 传递下去,也就是说新创建的协程会继承父协程的 CoroutineContext

CoroutineContext 数据的使用类似 Map,根据 Key 取值,如果子协程创建时指定了 CoroutineContext ,则会合并,相同 Key 的值会被覆盖。CoroutineContext 的主要目的是提供一个统一的方式来管理协程的执行环境和属性。

进入协程世界除了上面使用的 runBlocking 方式。还可以自定义 CoroutineScope

kotlin 复制代码
fun main() {
    CoroutineScope(Dispatchers.IO).launch {
			...
    }
}

上面代码创建了一个运行在 IO 线程环境的协程作用域并创建了一个协程,该协程继承了 CoroutineScopeCoroutineContext,所以运行在 IO 线程。

此外,还可以使用 GlobalScope 这个全局的协程作用域进入协程世界:

kotlin 复制代码
fun main() {
    GlobalScope.launch(Dispatchers.IO) {
			...
    }
}

💡 runBlocking、自定义`CoroutineScope`、`GlobalScope` 三者都可以进入协程世界,但实际使用时应该选择自定义的方式。runBlocking 会阻塞当前线程直到协程执行完毕,因此适合单元测试。`GlobalScope` 虽拿来即用,但它是全局的,生命周期太长,使用不当会导致内存泄露风险。

如果已经在协程世界中了,那么创建新的协程的方式就比较多了,launch、async、coroutineScope、supervisorScope 等都可以方便地创建不同需求的协程。

Dispatchers 与线程

Dispatchers 可以指定协程的执行的线程环境,不过它强调的是线程的类别而不是哪一个具体的线程。如:Dispatchers.IO 表示 IO 密集型线程池,Dispatchers.Default 表示 cpu 密集型线程池,特别的是,Dispatchers.Main 特指 Android 中的主线程。在协程中可以使用 withContext 进行线程池的切换:

kotlin 复制代码
fun main() {
    CoroutineScope(Dispatchers.Main).launch {
        withContext(Dispatchers.IO) {
            ...
        }
    }
}

💡 如果是网络数据传输等 io 任务,一定要使用 [Dispatchers.IO](http://Dispatchers.IO),其余计算类耗时任务使用 Dispatchers.Default,这是因为 Dispatchers.Default 线程池的线程数较少(和 cpu 核心数有关),而 [Dispatchers.IO](http://Dispatchers.IO) 线程池的线程数更多且可动态调整。io 任务往往等待时间更长,使用 Dispatchers.Default 的话很容易占满所有线程资源。

结构化并发

它是一种编程范式,旨在通过结构化的方式使并发编程 更清晰明确、更高质量、更易维护。

其核心有几点:

  • 通过把多线程任务进行结构化的包装,使其具有明确的开始和结束点,并确保其孵化出的所有任务在退出前全部完成。
  • 这种包装允许结构中线程发生的异常能够传播至结构顶端的作用域,并且能够被该语言原生异常机制捕获。

kotlin 协程设计中的协程关系、执行顺序、异常传播/处理 都符合结构化并发。结构化并发明确了并发任务什么时候开始,什么时候结束,异常如何传播,通过控制顶层结构具柄就可实现整个并发结构的取消、异常处理,使复杂的并发问题简单、清晰、可控。可以说,结构化并发大大降低了并发编程的难度。

相关推荐
闲暇部落2 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX4 小时前
Android 分区相关介绍
android
大白要努力!5 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee5 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood5 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-8 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen11 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年18 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX18 小时前
kotlin
开发语言·kotlin
建群新人小猿20 小时前
会员等级经验问题
android·开发语言·前端·javascript·php