Kotlin 协程是怎么样切换线程的

前言

用了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,内部继续追踪tryMakeCompletingSlowPathfinalizeFinishingState

    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上下文包装的分发器DispatchersCoroutineContext的重新装饰,使其具备不同的Dispatchers能力。

具体分析下从CoroutineScopelaunch方法:

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 协程 线程切换

结语

Kotlin的线程切换主要是用了装饰器模式,此模式在系统中使用频率还是很多的,最常见的就是我们使用的Context,在Activity和Fragment中都有Context,但他们的作用都不同,其实就是通过装饰器模式来进行一层装饰,从而是Context具备此组件特有的功能罢了。最后一句话,设计模式是真的厉害!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容