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

相关推荐
CV资深专家4 小时前
在 Android 框架中,接口的可见性规则
android
daifgFuture8 小时前
Android 3D球形水平圆形旋转,旋转动态更换图片
android·3d
雨白9 小时前
Kotlin 的延迟初始化和密封类
kotlin
二流小码农9 小时前
鸿蒙开发:loading动画的几种实现方式
android·ios·harmonyos
爱吃西红柿!10 小时前
fastadmin fildList 动态下拉框默认选中
android·前端·javascript
悠哉清闲11 小时前
工厂模式与多态结合
android·java
大耳猫11 小时前
Android SharedFlow 详解
android·kotlin·sharedflow
火柴就是我12 小时前
升级 Android Studio 后报错 Error loading build artifacts from redirect.txt
android
androidwork13 小时前
掌握 MotionLayout:交互动画开发
android·kotlin·交互
奔跑吧 android13 小时前
【android bluetooth 协议分析 14】【HFP详解 1】【案例一: 手机侧显示来电,但车机侧没有显示来电: 讲解AT+CLCC命令】
android·hfp·aosp13·telecom·ag·hf·headsetclient