文章目录
基本知识
概念解读
设计目的: 协程通过将复杂性放入库来简化异步编程,也就是用同步编程的方式来实现原有的异步编程。
常规函数是无法调用协程相关的特性,需要启动协程后使用。
协程依赖于线程,但是协程挂起时不需要阻塞线程
疑问:为什么不会阻塞线程?
答:参考非阻塞式挂起。
非阻塞式挂起
其本质就是切到其他线程,这样原来的线程就不阻塞了。
而给我们挂起后又执行,是底层帮我们当协程执行完成后,切换回原先线程。
基本使用
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
//可选,部分api必须,如MainScope
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
三种启动方式
1.runBlocking:T
会阻断当前线程,直到该协程执行结束。通常只用于启动最外层的协程,例如线程环境切换到协程环境。
2.launch:Job
该方式启动的协程任务是不会阻塞线程的,常用,一般适用不需要返回结果的。
3.async/await:Deferred
async定义异步job,await使用异步返回的结果。注意在async代码块中的异常,默认是抛弃的,需要通过await获取。
作用域,CoroutineScope
CoroutineScope是自行传播的
创建方式
val scope = CoroutineScope(Job() + Dispatchers.Main)
结构化并发(解决多协程并发的问题)
结构化并发的作用:
结构化并发能够保证当某个作用域被取消后,它内部所创建的所有协程也都被取消.
结构化并发保证了当 suspend 函数返回时,它所处理任务也都已完成。
通俗的理解:
通过作用域实现统一的协程执行策略(是一起取消还是互不干扰,又或者与上级协程作用域无关)。
如何做:
使用coroutineScope或者
supervisorScope来启动新协程。
可以解决的问题:
- 解决协程可能出现的泄漏问题。
- 使用作用域追踪,取消协程。
- 按照规定启动协程(官方已有)。
GlobeScope
自成一派:在全局的作用域中执行协程。并不受当前作用域影响。(默认作用域原则)
coroutineScope.launch
coroutineScope需要内部的协程完毕后才结束,在此期间是调用方是处于挂起状态。内部协程必须全部执行完成,有一个异常,则全部取消。
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
async { fetchDoc(2) }
}
}
supervisorScope
内部的子协程独立运行,互不干挠。执行期间调用方是挂起状态。
但子协程内的子协程依旧按默认作用域原则(这一项验证不通过,待处理)。
MainScope.launch
启动主线程中的协程,在普通非ui项目中,是不能使用。不过async方式可以用(即使不引入附属包)。
注意需要引入对应的android包。
viewModelScope
mvvm架构中的。
SupervisorJob
出异常的自己处理,不传递给父级,不影响其他协程。
val uiScope = CoroutineScope(SupervisorJob())
关键字解读
suspend
挂起关键字,挂起依靠内部协程逻辑,不能被常规函数调用。
resume
Job
join(),会等待自己以及所有子job的执行
Dispatcher
协程所在线程。如io。main等。
运用场景
并发
疑问:那这样kotlin的原生并发就没用了?
多层回调
view的生命周期
viewModelScope
异常处理
归纳:
官方指导
如果异常没有被处理,而且 CoroutineContext 没有一个 CoroutineExceptionHandler (稍后讲到) 时,异常会到达默认线程的 ExceptionHandler。在 JVM 中,异常会被打印在控制台;而在 Android 中,无论异常在那个 Dispatcher 中发生,都会导致您的应用崩溃。
未被捕获的异常一定会被抛出,无论您使用的是哪种 Job。
launch
scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) {
// 处理异常
}
}
async
当 async 被用作根协程 (CoroutineScope 实例或 supervisorScope 的直接子协程) 时不会自动抛出异常,而是在您调用 .await() 时才会抛出异常。
supervisorScope {
val deferred = async {
codeThatCanThrowExceptions()
}
try {
deferred.await()
} catch(e: Exception) {
// 处理 async 中抛出的异常
}
}
当 async 被用作根协程时,异常将会在您调用 .await 方法时被抛出
异常捕获
val handler = CoroutineExceptionHandler { context, exception ->
println("Caught $exception")
}
val uiScope = CoroutineScope(SupervisorJob())
uiScope.launch(handler) {
loadScope()
println("执行完毕")
}
suspend fun loadScope() {
supervisorScope {
launch {
delay(3000)
println("执行编号11")
}
launch {
delay(1000)
println("执行编号12")
launch {
delay(3000)
println("执行编号15")
}
throw StructuredConcurrencyWill("throw")
}
}
}
思考:结合这些特性,可以考虑对有需要捕获异常的地址,就地解决,不要带到外面来,防止不能正常的捕获。
自定义线程池
coroutineDispatcher为你定义的线程池
参考:https://www.kagura.me/dev/20190728214922.html
async(coroutineDispatcher) {
Thread.sleep(1000)
println("${Thread.currentThread().name}")
}
缺点
-
需要关注泄漏的问题,泄漏协程会浪费内存、CPU、磁盘资源,甚至发送一个无用的网络请求
-
在多线程数据共享或线程间数据精密控制不如java多线程更好处理。(待举证)
优点
- 可以实现用同步的方式写异步程序
- 可以更优雅的实现并发(协同)开发。
协程(Coroutine)就是协同程序,而Kotlin协程就是一个基于Java Thread API封装的工具包,帮助我们轻松的写出复杂的并发代码。其底层也是对线程池进行了包装处理(所以直接性能上,对比线程池没有变化)
迷惑代码
//给您下面一段代码,您能指出 Child 1 是用哪种 Job 作为父级的吗?
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
// new coroutine -> can suspend
launch {
// Child 1
}
launch {
// Child 2
}
}
答案:Job,
原因:scope.launch并不能重置构造方法传入的job。所以子协程依旧是job。