使用协程需要引入
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
1.什么是协程
官方文档(本质上,协程是轻量级的线程。)
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
个人理解:协程是一个线程框架,协程就是方法调用封装成类线程的API。
使用协程
启动
协程需要运行在协程上下文环境,在非协程环境中凭空启动协程,有三种方式
runBlocking{}
启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用。
GlobalScope.launch{}
在应用范围内启动一个新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。
由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。
CoroutineScope + launch{}
这是在应用中最推荐使用的协程使用方式——为自己的组件实现CoroutieScope接口,在需要的地方使用launch{}方法启动协程。使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁。从而实现安全可靠地协程调用。
在一个协程中启动子协程,一般来说有两种方式
launch{}
异步启动一个子协程
async{}
异步启动一个子协程,并返回Deffer对象,可通过调用Deffer.await()方法等待该子协程执行完成并获取结果,常用于并发执行-同步等待的情况
下面举个栗子
class Test CoroutineScope(): CoroutineScope {
override fun getList(handler: Handler<Result<Response>>){
launch{
val deffer1 = async{ awaitResult<List<JsonObject>>{ dbService.getContentList(it) } }
val deffer2 = async{ awaitResult<List<JsonObject>>{ dbService.getAuthorList(it) } }
val contents = deffer1.await()
val authors = deffer2.await()
val reuslt = contents.map{ content ->
content.put("author", authors.filter{ ... }.first())
}
resultHandler.succeed(reuslt)
}
}
}
协程的取消
launch{}返回Job,async{}返回Deffer,Job和Deffer都有cancel()取消协程。
取消自协程不影响父协程,取消父协程,子协程也取消。
从协程内部看取消的效果
- 标准库的挂起方法会抛出CancellationException异常。
- 用户自定义的常规逻辑并不会收到影响,除非我们手动检测isActive标志。
一个栗子
val job = launch {
// 如果这里不检测isActive标记,协程就不会被正常cancel,而是执行直到正常结束
while (isActive) {
......
}
}
job.cancelAndJoin()
学习了启动跟取消,来看看协程异常。
异常
Kotlin协程的异常有两种
- 因协程取消,协程内部suspend方法抛出的CancellationException
- 常规异常,这类异常,有两种异常传播机制
- launch:将异常自动向父协程抛出,将会导致父协程退出
- async: 将异常暴露给用户(通过捕获deffer.await()抛出的异常)
上一个官方栗子
fun main() = runBlocking {
val job = GlobalScope.launch { // root coroutine with launch
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // 我们将在控制台打印 Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = GlobalScope.async { // root coroutine with async
println("Throwing exception from async")
throw ArithmeticException() // 没有打印任何东西,依赖用户去调用等待
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
控制台输出
Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException
全局异常处理(CoroutineExceptionHandler)
launch(CoroutineExceptionHandler { _, e ->
logger.error("Exception when get content list.", e)
resultHandler.fail()
}) {
val deffer1 = async{ awaitResult<List<JsonObject>>{ dbService.getContentList(it) } }
val deffer2 = async{ awaitResult<List<JsonObject>>{ dbService.getAuthorList(it) } }
val contents = deffer1.await()
val authors = deffer2.await()
val reuslt = contents.map{ content ->
content.put("author", authors.filter{ ... }.first())
}
}
协程上下文
顾名思义,协程上下文表示协程的运行环境,包括协程调度器、代表协程本身的Job、协程名称、协程ID等。通过CoroutineContext定义,CoroutineContext被定义为一个带索引的集合,集合的元素为Element,上面所提到调度器、Job等都实现了Eelement接口。
由于CoroutineContext被定义为集合,因此在实际使用时可以自由组合加减各种上下文元素。
启动子协程时,子协程默认会继承除Job外的所有父协程上下文元素,创建新的Job,并将父Job设置为当前Job的父亲。
启动子协程时,可以指定协程上下文元素,如果父上下文中存在该元素则覆盖,不存在则添加。
// 自定义新协程名称
launch(CoroutineName("customName")){
... ...
}
调度器
调度器是协程上下文中众多元素中最重要的一个,通过CoroutineDispatcher定义,它控制了协程以何种策略分配到哪些线程上运行。这里介绍几种常见的调度器
Dispatcher.Default
默认调度器。它使用JVM的共享线程池,该调度器的最大并发度是CPU的核心数,默认为2
Dispatcher.Unconfined
非受限调度器,它不会将操作限制在任何线程上执行——在发起协程的线程上执行第一个挂起点之前的操作,在挂起点恢复后由对应的挂起函数决定接下来在哪个线程上执行。
Dispathcer.IO
IO调度器,他将阻塞的IO任务分流到一个共享的线程池中,使得不阻塞当前线程。该线程池大小为环境变量kotlinx.coroutines.io.parallelism的值,默认是64或核心数的较大者。
该调度器和Dispatchers.Default共享线程,因此使用withContext(Dispatchers.IO)创建新的协程不一定会导致线程的切换。
Dispathcer.Main
该调度器限制所有执行都在UI主线程,它是专门用于UI的,并且会随着平台的不同而不同
对于JS或Native,其效果等同于Dispatchers.Default
对于JVM,它是Android的主线程、JavaFx或者Swing EDT的dispatcher之一。
并且为了使用该调度器,还必须增加对应的组件
kotlinx-coroutines-android
kotlinx-coroutines-javafx
kotlinx-coroutines-swing
其它
在其它支持协程的第三方库中,也存在对应的调度器,如Vertx的vertx.dispatcher(),它将协程分配到vertx的EventLoop线程池执行。
注意,由于上下文具有继承关系,因此启动子协程时不显式指定调度器时,子协程和父协程是使用相同调度器的。
Job
Job也是上下文元素,它代表协程本身。Job能够被组织成父子层次结构,并具有如下重要特性。
父Job退出,所有子job会马上退出
子job抛出除CancellationException(意味着正常取消)意外的异常会导致父Job马上退出
类似Thread,一个Job可能存在多种状态
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New (optional initial state) | false | false | false |
Active (default initial state) | true | false | false |
Completing (transient state) | true | false | false |
Cancelling (transient state) | false | false | true |
Cancelled (final state) | false | true | true |
Completed (final state) | false | true | false |
作用域
协程作用域——CoroutineScope,用于管理协程,管理的内容有
- 启动协程的方式 - 它定义了launch、async、withContext等协程启动方法(以extention的方式),并在这些方法内定义了启动子协程时上下文的继承方式。
- 管理协程生命周期 - 它定义了cancel()方法,用于取消当前作用域,同时取消作用域内所有协程。
fun test(){
viewModelScope.launch(Dispatchers.Main) {
print("1:" + Thread.currentThread().name)
withContext(Dispatchers.IO){
delay(1000)
print("2:" + Thread.currentThread().name)
}
print("3:" + Thread.currentThread().name)
}
}
//1,2,3处分别输出main,DefaultDispatcher-worker-1,main
区分作用域和上下文
从类定义看,CoroutineScope和CoroutineContext非常类似,最终目的都是协程上下文,但正如Kotlin协程负责人Roman Elizarov在Coroutine Context and Scope中所说,二者的区别只在于使用目的的不同——作用域用于管理协程;而上下文只是一个记录协程运行环境的集合。
Flow
我的理解跟rxjava 差不多,感兴趣可以看官方文档。
网友评论