前言
用了kotlin的协程很久了,都说协程是轻量级的线程,是用户态,资源消耗比系统态的线程切换要少很多,可是协程不也是高度封装的线程池吗?从IO切换到MAIN难道就不需要线程间的切换了吗?既然也涉及到线程切换,那为何又比直接切换线程要节省资源消耗呢?这些疑问一直没解开,今天就尝试着解开这个疑问。
前置知识点
要了解协程是如何切换线程的,最好是先了解下协程的一些知识点。
1.启动方式
CoroutineStart.DEFAULT
:协程创建后,立即开始调度,但 有可能在执行前被取消。在调度前如果协程被取消,其将直接进入取消响应的状态。
CoroutineStart.LAZY
:只要协程被需要时(主动调用该协程的 start、 join、 await等函数时 ), 才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态。
CoroutineStart.ATOMIC
:协程创建后,立即开始调度, 协程执行到第一个挂起点之前不响应取消。其将调度和执行两个步骤合二为一,就像它的名字一样,其保证调度和执行是原子操作,因此协程也 一定会执行。
CoroutineStart.UNDISPATCHED
:协程创建后,立即在当前线程中执行,直到遇到第一个真正挂起的点。是立即执行,因此协程 一定会执行。
日常使用最多的就是CoroutineStart.DEFAULT
,也是默认的启动方式,一般我们不去配置启动方式的话,就是默认的default了。
2.上下文
Job:工作空间。用于启动or取消协程。
Dispatchers:
- Default:默认调度器 ,适合处理后台计算,其是一个 CPU 密集型任务调度器。
- IO:IO 调度器,适合执行 IO 相关操作,其是 IO 密集型任务调度器。
- Main:UI 调度器,根据平台不同会被初始化为对应的 UI 线程的调度器, 在Android 平台上它会将协程调度到 UI 事件循环中执行,即通常在 主线程上执行。
- Unconfined:“无所谓“调度器,不要求协程执行在特定线程上。
CoroutineExceptionHandler:全局异常捕获(只能在根协程配置)。
CoroutineName:协程名称。
协程上下文就是CoroutineContext
,其中可以用加和函数plus()
来连接使用,比如:
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + handler
这里的+
就是加和函数,如上所写就是让CoroutineContext
具备主线程+工作空间job,和CoroutineExceptionHandler的能力。
3.作用域
顶级作用域:GlobalScope-->全局范围,不会自动结束执行,无法取消。
协同作用域:coroutineScope -->抛出异常会取消父协程
主从作用域:supervisorScope -->抛出异常,不会取消父协程
三种作用域真正常用的其实只有主从作用域,谁也不想让自己写的协程挂了导致app崩溃吧。但实际使用过程中,由于没有作用域的概念,往往会用到顶级作用域和协同作用域,协程挂了导致app崩溃,然后再去解决异常。
常用的主从作用域我们也肯定接触过:
- MainScope:主线程的作用域,全局范围,可以取消。
- lifecycleScope: 生命周期范围,用于activity等有生命周期的组件,在Desroyed的时候会自动结束。
- viewModelScope:ViewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束。
上面的Scope本质都是主从作用域,此方式启动的协程,崩溃后不会影响其他协程执行。
那为什么主从作用域发生异常不会影响其他协程呢?我们以MainScope为例看看源码:
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
MainScope会初始化一个上下文作用域,分别包括SupervisorJob()
和Dispatchers.Main
,那本质上应该就在SupervisorJob()
中,继续往下看源码:
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
override fun childCancelled(cause: Throwable): Boolean = false
}
重写了childCancelled
返回false,这个返回值其实就是表示子协程异常不会取消其他协程执行。
继续看父类:
internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob {
init { initParentJob(parent) }
override val onCancelComplete get() = true
/*
* Check whether parent is able to handle exceptions as well.
* With this check, an exception in that pattern will be handled once:
* ```
* launch {
* val child = Job(coroutineContext[Job])
* launch(child) { throw ... }
* }
* ```
*/
override val handlesException: Boolean = handlesException()
override fun complete() = makeCompleting(Unit)
// 关键方法
override fun completeExceptionally(exception: Throwable): Boolean =
makeCompleting(CompletedExceptionally(exception))
@JsName("handlesExceptionF")
private fun handlesException(): Boolean {
var parentJob = (parentHandle as? ChildHandleNode)?.job ?: return false
while (true) {
if (parentJob.handlesException) return true
parentJob = (parentJob.parentHandle as? ChildHandleNode)?.job ?: return false
}
}
}
首先会初始化一个根协程:initParentJob(parent)
,查看源码可以发现,如果没有我们没有主动配置job,会默认创建一个根协程。
继续分析异常抓取方法:
internal fun makeCompleting(proposedUpdate: Any?): Boolean {
loopOnState { state ->
val finalState = tryMakeCompleting(state, proposedUpdate)
when {
finalState === COMPLETING_ALREADY -> return false
finalState === COMPLETING_WAITING_CHILDREN -> return true
finalState === COMPLETING_RETRY -> return@loopOnState
else -> {
afterCompletion(finalState)
return true
}
}
}
}
继续追踪tryMakeCompleting
,内部继续追踪tryMakeCompletingSlowPath
,finalizeFinishingState
:
private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
...省略
// Now handle the final exception
if (finalException != null) {
val handled = cancelParent(finalException) || handleJobException(finalException)
if (handled) (finalState as CompletedExceptionally).makeHandled()
}
...省略
}
handleJobException(finalException)
不重写的话默认是false,主要看cancelParent(finalException)
继续追踪
private fun cancelParent(cause: Throwable): Boolean {
// Is scoped coroutine -- don't propagate, will be rethrown
if (isScopedCoroutine) return true
/* CancellationException is considered "normal" and parent usually is not cancelled when child produces it.
* This allow parent to cancel its children (normally) without being cancelled itself, unless
* child crashes and produce some other exception during its completion.
*/
val isCancellation = cause is CancellationException
val parent = parentHandle
// No parent -- ignore CE, report other exceptions.
if (parent === null || parent === NonDisposableHandle) {
return isCancellation
}
// Notify parent but don't forget to check cancellation
return parent.childCancelled(cause) || isCancellation
}
可以看到最后调用了childCancelled(cause)
,默认而SupervisorJobImpl
重写了此方法,返回false,而正常来说isCancellation
肯定是false,不会是CancellationException
。
至此看到异常不会继续向上传递,从而不会取消父协程,也不会导致其他子协程挂掉。
线程切换
以上简单的介绍了协程的相关概念,理解以上概念后,才会更好的理解协程是怎么样进行的线程切换。
协程的线程切换说简单也很简单,简单到一个设计模式就搞定:装饰器模式
。
如果你理解什么是装饰器模式,那对于理解协程的线程切换就非常简单,无非就是CoroutineContext
上下文包装的分发器Dispatchers
对CoroutineContext
的重新装饰,使其具备不同的Dispatchers
能力。
具体分析下从CoroutineScope
的launch
方法:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
所有的launch
方法都走到这,这就是入口。从这可以看到会先执行newCoroutineContext(context)
,此方法是对传入的上下文CoroutineContext
进行一次包装:
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
// 此处的+是加和函数,不是常规意义的加号
val combined = coroutineContext + context
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
// 如果没有配置线程,默认使用Dispatchers.Default
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}
通过加和函数将旧的上下文和新的上下文能力整合到一起,新的能力覆盖旧的。而线程如果不配置,默认是Dispatchers.Default
。
继续分析launch
方法,非lazy会创建一个标准的StandaloneCoroutine
,随后执行start
方法:
/**
* Starts this coroutine with the given code [block] and [start] strategy.
* This function shall be invoked at most once on this coroutine.
*
* * [DEFAULT] uses [startCoroutineCancellable].
* * [ATOMIC] uses [startCoroutine].
* * [UNDISPATCHED] uses [startCoroutineUndispatched].
* * [LAZY] does nothing.
*/
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
start(block, receiver, this)
}
方法很简单,主要看注释,[DEFAULT] uses [startCoroutineCancellable]
/**
* Similar to [startCoroutineCancellable], but for already created coroutine.
* [fatalCompletion] is used only when interception machinery throws an exception
*/
internal fun Continuation<Unit>.startCoroutineCancellable(fatalCompletion: Continuation<*>) =
runSafely(fatalCompletion) {
// 先判断是否有拦截器,然后执行resumeCancellableWith
intercepted().resumeCancellableWith(Result.success(Unit))
}
继续追踪:
public fun <T> Continuation<T>.resumeCancellableWith(
result: Result<T>,
onCancellation: ((cause: Throwable) -> Unit)? = null
): Unit = when (this) {
is DispatchedContinuation -> resumeCancellableWith(result, onCancellation)
else -> resumeWith(result)
}
此方法就在DispatchedContinuation
中,所以必然会进入resumeCancellableWith
方法中:
inline fun resumeCancellableWith(
result: Result<T>,
noinline onCancellation: ((cause: Throwable) -> Unit)?
) {
val state = result.toState(onCancellation)
// 判断当前上下文是否需要重新分发,如果需要就将上下文中提取新的Dispathers赋给dispatcher,否则就在当前线程直接执行
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_CANCELLABLE
dispatcher.dispatch(context, this)
} else {
executeUnconfined(state, MODE_CANCELLABLE) {
if (!resumeCancelled(state)) {
resumeUndispatchedWith(result)
}
}
}
}
如果线程有变,就会执行到if判断中,最终中dispatcher.dispatch(context, this)
。此方法是个抽象方法,具体执行要去看看切换的是哪个线程,比如主线程Dispatchers.Main
,最终就是走到了HandlerContext
依靠Handler
来进行主线程切换:
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)
@JvmField
val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()
private fun loadMainDispatcher(): MainCoroutineDispatcher {
return try {
val factories = if (FAST_SERVICE_LOADER_ENABLED) {
FastServiceLoader.loadMainDispatcherFactory()
} else {
...
}
...
} catch (e: Throwable) {
// Service loader can throw an exception as well
createMissingDispatcher(e)
}
}
internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
...
return try {
val result = ArrayList<MainDispatcherFactory>(2)
createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
result
} catch (e: Throwable) {
// Fallback to the regular SL in case of any unexpected exception
load(clz, clz.classLoader)
}
}
// 通过反射创建AndroidDispatcherFactory
internal class AndroidDispatcherFactory : MainDispatcherFactory {
override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
HandlerContext(Looper.getMainLooper().asHandler(async = true))
// HandlerContext继承MainCoroutineDispatcher,MainCoroutineDispatcher继承抽象类CoroutineDispatcher,HandlerContext重写CoroutineDispatcher的dispatch,从而完成主线程切换
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}
至此主线程的切换就讲完了,其他几个切换也类似此流程,主要是Dispatcher
不同,比如Dispatchers.Default
是创建了一个默认的线程池,而Dispatchers.IO
也是沿用的线程池,只是对线程数量做了限制罢了。
以上的流程简单可以看流程图
结语
Kotlin的线程切换主要是用了装饰器模式,此模式在系统中使用频率还是很多的,最常见的就是我们使用的Context
,在Activity和Fragment中都有Context
,但他们的作用都不同,其实就是通过装饰器模式来进行一层装饰,从而是Context
具备此组件特有的功能罢了。最后一句话,设计模式是真的厉害!!