一文搞懂kotlin协程
基础概念
- 协程:是可挂起计算的实例。它在概念上类似于线程,因为它需要一个代码块来运行,该代码块与其余代码同时工作。但是,协程不绑定到任何特定线程。它可能会在一个线程中暂停执行,并在另一个线程中恢复执行。
- 结构化并发:协程的生命周期必须与作用域绑定,形成父子关系树。
- 挂起:挂起是协程在执行过程中主动让出线程的能力,挂起函数用suspend修饰。
协程上下文CoroutineContext
val defaultScope =
CoroutineScope(SupervisorJob() + CoroutineName("hello Default") + Dispatchers.Default + CoroutineExceptionHandler { context, e ->
})
-
Job, 控制协程的生命周期,一般使用
Job()或者SupervisorJob()。Job():子协程失败会取消父协程和兄弟协程SupervisorJob():子协程失败不影响其他协程
-
CoroutineName,给协程上下文命名,默认是NULL,调试方便。
-
Dispatchers,将协程调度到不同类型的线程。
Dispatchers.Default: cpu密集型计算场景,使用共享线程池(固定大小,线程数=CPU核心数)Dispatchers.IO: 耗时操作场景,使用动态扩展线程池(优先使用共享线程池)Dispatchers.Main: 主线程Dispatchers.Unconfined: 不推荐
-
CoroutineExceptionHandler,协程上下文异常处理
val thread: Thread get() = Thread.currentThread()
val ioScope =
CoroutineScope(SupervisorJob() + CoroutineName("hello IO") + Dispatchers.IO)
fun main(): Unit = runBlocking {
launch {
println("hello launch_1: $thread + ${coroutineContext[CoroutineName]} + ${coroutineContext[Job]}")
}
launch(ioScope.coroutineContext) {
println("hello launch_2: $thread + ${coroutineContext[CoroutineName]} + ${coroutineContext[Job]}")
}
}
//输出:
hello launch_2: Thread[#30,DefaultDispatcher-worker-1,5,main] + CoroutineName(hello IO) + StandaloneCoroutine{Active}@4e9f373
hello launch_1: Thread[#1,main,5,main] + null + StandaloneCoroutine{Active}@305fd85d
启动协程
runBlocking
连接了常规fun main()的非协程世界和runBlocking { ... }大括号内的协程代码, 会阻塞当前线程直到所有内容执行完成。
仅在测试中使用,生产代码用不到。
launch
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job
当前协程的扩展方法,在当前协程下启动一个新的协程,返回的是Job对象,出现异常立刻传播,适用于不需要返回值的场景。
val job = launch {
println("launch_1: ${coroutineContext[Job]}")
}
println("job: $job")
//输出:
job: StandaloneCoroutine{Active}@1f7030a6
launch_1: StandaloneCoroutine{Active}@1f7030a6
async
public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred<T>
当前协程的扩展方法,在当前协程下启动一个新的协程,返回Deferred<T>, 使用deferred.await()获取结果,异常在await时才会传播。
val deferred = async {
delay(2000)
"hello, sync"
}
deferred.await()
coroutineScope和supervisorScope
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R
挂起函数,上下文只能继承于父协程,主要作用是控制包含的子协程。
- coroutineScope:子协程失败则所有取消
- supervisorScope:子协程失败不影响其他协程
coroutineScope {
println("coroutineScope: start")
launch {
delay(2000)
println("normal...")
}
launch {
throw Exception("error...")
}
println("coroutineScope: end")
}
//输出:
使用coroutineScope时,println("normal...")并不会执行,因为第二个子协程抛了异常。
将coroutineScope替换为supervisorScope,异常仍然抛,但是println("normal...")仍然会执行。
withContext
public suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): T
挂起函数,相比coroutineScope和supervisorScope多了协程上下文参数,一般传入协程调度器切换协程所在的线程
println("global: ${coroutineContext[Job]}")
withContext(Dispatchers.IO) {
println("withContext: ${coroutineContext[Job]?.parent}")
}
//输出:相同的job对象
global: BlockingCoroutine{Active}@51cdd8a
withContext: BlockingCoroutine{Active}@51cdd8a
GlobalScope
生命周期绑定应用进程的作用域,一般不使用它来开辟协程。
协程启动模式
launch和sync的声明中,除了协程上下文参数,还有一个CoroutineStart,代表协程的启动模式
CoroutineStart.DEFAULT
立即调度,可以立刻取消。
CoroutineStart.LAZY
不会立即调度,需要显示的使用Job的start(),join(),await()才会启动,可以立刻取消。
CoroutineStart.ATOMIC
立即调度,但是不能立刻取消,需要执行到第一个挂起点才行。
val job = launch(start = CoroutineStart.ATOMIC) {
println("launch----1---")
delay(1000)
println("launch--2-----")
}
job.cancelAndJoin()
//输出,会打印"launch----1---",换成DEFAULT的话就不会打印。
CoroutineStart.UNDISPATCHED
立即在当前线程执行,直到第一个挂起点后,在根据调度器去切换线程或者在其他上下文执行。
launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.IO) {
println("launch_1 thread: $thread")
delay(1000)
println("launch_2 thread: $thread")
}
println("main....")
//输出:第一个日志打印会现在主线程执行,到达第一个挂起点delay时再根据传入的调度器切换到线程池。
launch_1 thread: Thread[#1,main,5,main]
main....
launch_2 thread: Thread[#31,DefaultDispatcher-worker-1,5,main]
协程的生命周期
一定要阅读Job接口的类注解
通过打印协程上下文可以看到当前协程处于的状态,比如job=StandaloneCoroutine{Cancelling}@3e57cd70
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
初始状态
协程已构建,但是还没有调度执行,通过CoroutineStart.LAZY启动协程到这一阶段。
isActive = false, isCompleted = false, isCancelled = false
Active状态
通过job.isActive可以检查当前是否处于激活状态,挂起时仍然处于激活状态。
isActive = true, isCompleted = false, isCancelled = false
手动激活协程的方式:
job.start(),启动job的相关的协程,如果启动成功返回true,如果协程已经启动或者已完成则返回false。job.join(),join是一个挂起函数,会阻塞当前的协程,直到job相关的协程执行完,调用join的job如果处于未激活状态,则会激活。deferred.await(),和join类似,会阻塞当前协程,区别是await会得到async协程的返回值。
Completed状态
协程执行完成或者return后会走到Completed状态,通过job.isCompleted查看。
isActive = false, isCompleted = true, isCancelled = false
当协程块中抛出异常结束协程,协程isCompleted也为true。
Cancelled状态
通过job.isCancelled检查协程是否触发了取消。
isActive = false, isCompleted = true, isCancelled = true
- 调用
job.cancel()或者job.cancelAndJoin()取消协程 - 协程内抛出了异常。
协程的取消不是立即执行的,需要遇到挂起点或者手动调用状态函数,比如
isActive才会触发取消。
Completing
完成前的过渡状态。
isActive = true, isCompleted = false, isCancelled = false
Cancelling
取消前的过渡状态。
isActive = false, isCompleted = false, isCancelled = true
协程的异常处理
协程的异常遵循结构化并发,子协程的异常会传播到父协程和兄弟协程,SupervisorJob协程内则不会传播。
异常处理方式
- 将协程块中的代码try-catch起来。
- 在当前协程上下文中的CoroutineExceptionHandler捕获。
CancellationException
协程取消时抛出的特殊异常,静默处理。
- 当需要在取消时做逻辑操作时,catch捕获CancellationException后需要继续抛出。
- 正常清理逻辑不需要捕获CancellationException,只需要在finally清理即可。
在finally中想要执行挂起函数,需要包含在
withContext(NonCancellable)。
- 一般使用中,不需要手动抛出CancellationException。
TimeoutCancellationException
超时异常是CancellationException的子类,超时后协程自动取消,并抛出异常。
try {
val result = withTimeout(1000) { // 1秒超时
fetchDataFromNetwork() // 挂起函数
}
println("结果: $result")
} catch (e: TimeoutCancellationException) {
println("操作超时")
throw e
}
withTimeout:挂起函数,指定超时时间,代码块中的在指定时间内没有执行完成就抛出异常并取消,返回代码块最后一行。withTimeoutOrNull:和withTimeout的区别时当超时取消时会返回null。
超时逻辑中,防止内存泄露,不要在超时函数后紧跟资源释放逻辑,应该放在finally中。
更多推荐
所有评论(0)